02 NDS 홈브루(Homebrew) - NDS 윈도우 시스템(Windows System)

원문 :  http://kkamagui.springnote.com/pages/417428

 

들어가기 전에...

 

 

0.버전관리

  • 2007/10/18 21:12:24 : MovableWnd가 없어지고 Move 기능이 별도의 클래스로 이동함. 타이틀바를 클릭했을 때, 윈도우 이동을 지원하고 싶으면 CMovable 클래스를 사용하면 됨

 

0.시작하면서...

  내가 만든 NDS 윈도우 시스템(Window System)에 대한 문서이다. NDS 윈도우 시스템은 최대한 윈도우의 MFC 구조와 비슷하게 하여 윈도우 프로그래밍에 익숙한 사람이라면 누구나 쉽게 접근할 수 있도록 하는 것이 목적이다.

 현재 Base Window와 DC 정도만 구현되어있으며, 이것을 활용하여 만들어진 홈브류는 00 KKAMAGUI NOTEPAD 가 있다.

 현재 전체적인 윈도우의 구조는 아래와 같이 구성되어있다.

 클래스다이어그램.PNG

 <전체 클래스 다이어그램>

 주의할 점은 아래와 같다. 

  • 최대한 비슷하게 만들려는 의도지만, 여건상 메시지 처리 부분과 함수 호출의 순서에 약간 차이가 있을 수 있다. 따라서 윈도우 프로그래밍에 대한 지식으로 접근하면 제대로 동작하지 않을 수 있다.
  • 자동으로  처리되는 것은 거의 없다. 하다못해 ShowWindow() 함수를 이용해서 윈도우를 숨겼을 때, 단순히 윈도우를 그려주지만 않을 뿐 나머지 처리는 알아서 다 해야한다. 즉 원하는 게 있으면 전부 손으로 작성해야 한다.

 

 참고할 점은 아래와 같다.

  • 라이브러리는 RGB555 포맷을 사용하는 Frame Buffer 모드를 기본으로 사용한다고 가정하고 제작되었다.

    • 만약 다른 모드를 사용한다면 2D Raster 함수 부분을 적절하게 수정해야 할 것이다. 
    • 아래는 Frame Buffer 모드로 설정하는 예제이다. 
    1. /**
          Main LCD 및 SUB LCD를 모두 16bit 256 * 196 로 설정한다.
              Frame Buffer와 같이 쓸 수 있도록 수정한다.
      */
      void InitVideoMode()
      {
          // 16bit Color Mode 5
          videoSetMode( MODE_5_2D | DISPLAY_BG3_ACTIVE );
          videoSetModeSub( MODE_5_2D | DISPLAY_BG3_ACTIVE );
          
          // Video Memory를 설정한다. MAIN 같은 경우는 2개의 VRAM이 맵핑되어있으므로
          // 더블 버퍼의 사용도 가능하다.
          vramSetMainBanks( VRAM_A_MAIN_BG_0x06000000, VRAM_B_MAIN_BG_0x06020000,
                            VRAM_C_SUB_BG, VRAM_D_LCD);
    2.     // Background에 대한 설정을 한다. BG_BMP_BASE를 조절하면 스크롤 및
          // 더블 버퍼를 구현할 수 있다.
          BG3_CR = BG_BMP16_256x256 | BG_BMP_BASE( 0 ); //| BG_PRIORITY( 3 );
          // scale을 1, rotation을 0으로 설정하여 frame buffer와 같게 만듬
          BG3_XDX = 1 << 8;
          BG3_XDY = 0;
          BG3_YDX = 0;
          BG3_YDY = 1 << 8;
          // Translation(Reference Point X/Y-Coordinate)을 0으로 설정
          // 표시하는 위치를 옮기고 싶으면 조절
          BG3_CX = 0;
          BG3_CY = 0;
          // x축 및 y축으로 100 pixel 이동
          //BG3_CX = 100 << 8; 
          //BG3_CY = 100 << 8;
              
          SUB_BG3_CR = BG_BMP16_256x256 | BG_BMP_BASE( 0 );// | BG_PRIORITY( 3 );
          SUB_BG3_XDX = 1 << 8;
          SUB_BG3_XDY = 0;
          SUB_BG3_YDX = 0;
          SUB_BG3_YDY = 1 << 8;
          SUB_BG3_CX = 0;
          SUB_BG3_CY = 0;
  • 화면의 크기는 매크로로 정의되어있다. 화면이 더 넓어지거나 좁아진다면 수정해야 한다.

 

1.2D 래스터 연산(Raster Operation)

 윈도우 라이브러리는 클립핑이 지원되는 라인 그리기 함수 및 사각형 그리기 함수를 지원한다.

 라인을 그리는 함수는 Bresenham 알고리즘을 사용하고 클립핑 알고리즘으로는 cohen-sutherland 알고리즘을 이용했다. 사각형 타입은 Fill Rect와 Rect 두가지 타입이 있는데, Rect는 라인을 그리는 함수를 이용하여 구현되었다.

 2D Raster 함수들은 클립핑 영역을 받도록 되어있는데, 일단 기본 설정은 스크린에 그리는 것으로 고정되어있다. 따라서 Y 좌표의 계산은 Y * SCREEN_WIDTH로 설정되어있으며 추후 스크린이 아닌 메모리 영역에 그릴 경우 이 부분을 수정할 필요가 있다.

 2D Raster 함수들은 아래와 같이 구성되어있고 CDC 클래스에서 사용된다.

  1. void DrawLine( RECT stClippingRect, void* pvAddr, int iX1, int iY1, int iX2,
            int iY2, unsigned short usColor );
    void DrawBox( RECT stClippingRect, void* pvAddr, int iX1, int iY1, int iX2,
            int iY2, unsigned short usColor, bool bFill );
    void DrawPixel( RECT stClippingRect, void* pvAddr, int iX, int iY,
            unsigned short usColor );

 

2.한글/영문 출력

 한글/영문 출력 부분은 자작한 폰트 생성 툴을 이용해서 비트마스크 폰트를 생성한 후 이를 출력하는 방법으로 구현하였다. 물론 클립핑에 대한 처리도 되어있다.

 현재 폰트는 12x12 크기의 한글 폰트와  6x12 크기의 영문 폰트로 구성되어있다. Font 출력에 대한 함수들은 아래와 같이 구성되어있고 CDC 클래스에서 사용된다.

  1. #define ENG_FONT_WIDTH  6
    #define HAN_FONT_WIDTH  12
    #define ENG_FONT_HEIGHT 12
    #define HAN_FONT_HEIGHT 12
  2. // 한글/영어 다 찍을 수 있다.
    void PrintChar( RECT stClippingRect, void* pvBaseAddr, int iX, int iY,
            unsigned short usForeColor, unsigned short usBackColor, char cChar );
    void PrintString( RECT stClippingRect, void* pvBaseAddr, int iX, int iY,
            unsigned short usForeColor, unsigned short usBackColor, char* pcString );

 한글 폰트 생성 및 출력에 대한 내용은 01 NDS 한글 출력 라이브러리 문서를 참고하자.

 

3.Z-Order 처리

 Z-Order는 간단하게 구현되어있다. 윈도우 메니져가 Z-Order의 첫번째 윈도우만 관리하고 나머지는 윈도우 자신이 다음에 위치하는 윈도우의 포인터를 가지는 방식으로 관리한다. 이렇게 함으로써 윈도우 메니져의 자료구조를 단순하게 하고 윈도우 관리에 필요한 추가적인 자료구조를 없앴다.

 Z-Order의 구성은 윈도우를 화면에 그리는 순서와도 관계가 있는데 Z-Order의 Bottom에서부터 Z-Order의 Top 순으로 OnPaint() 함수를 호출해서 그린다.

 아래는 Z-Order의 순서와 실제 화면과의 관계를 나타낸 그림이다.

윈도우라이브러리1.PNG

<Z-Order & Screen>

 

 

4.윈도우 그리기 및 그리는 최소 영역 설정

 윈도우즈나 X-Window 같은 윈도우 시스템의 경우, 무식하게 화면 전체를 계속해서 갱신하지 않는다. 또한 윈도우 하나의 내용이 갱신되었다고 해서 윈도우 전체를 다시 그리는 일도 하지 않는다. 변화되면 해당 부분만 그려서 그리는 영역을 최소화하고, 이를 효율적으로 관리하여 효율을 최대한 높이기 위해 노력한다(화면을 그리는데 너무 많은 processing power를 사용한다면 일은 언제하는가? ㅡ_ㅡ;;;)

 처음에는 세세한 부분까지 변화된 영역을 검출하여 처리하려 했다가 알고리즘이 너무 복잡해지는 바람에 생략했었다. 뭐 사실 대충만든 프로토타입이 잘 작동해준 원인도 있지만... 윈도우 라이브러리를 계속 개발하다보니 속도가 점점 떨어지는 것이 눈에 보여서 어쩔 수 없이 넣게 되었다.

 윈도우 라이브러리에서 효율을 높이기위해 사용하는 방식은 3가지다.

  • 더블 버퍼링 : 메인 메모리를 할당하여 윈도우를 그리고 완전히 그려진 뒤에 비디오 메모리로 고속 전송한다. 고속 전송 함수는 간단히 어셈으로 짰다. ㅡ_ㅡ;;; 속도가 의심스러운 사람도 있을텐데, memcpy 함수보다 2배는 빠르니 믿어도 된다.(프로파일링해서 나온 결과다. 프로파일러에 대해서는  22 타이머(Timer)를 이용한 프로파일러(Profiler) 만들기 를 참고하자.) 
  • 일괄 그리기 : 윈도우가 변화되었다고 해서 버퍼에 직접 그리지 않는다. 윈도우를 다시 그려야 한다는 플래그와 영역만을 윈도우 매니저에게 알리고 윈도우 매니져는 메시지 처리가 끝난 뒤 일괄적으로 윈도우를 다시 그린다.
  • 그리기 영역 설정 : 조그만 윈도우가 변했다고 전체를 다시 그리는건 문제가 있다. 따라서 그려야 할 영역이 있을 때 이 영역들을 단순한 사각형 더하기로 계산하여 해당 영역에 있는 윈도우만 다시 그리도록 한다. 간단하지만 의외로 효율이 괜찮다. 

  이제 하나하나에 대해서 자세히 알아보자.

 

4.1 더블 버퍼링(Double Buffering)

 게임을 개발한 사람 or 윈도우 그림판 같은 프로그램을 개발해본 사람이라면 더블 버퍼링을 모르는 사람이 없을 것이다. 다행이도 NDS에는 비디오 메모리가 많기 때문에 비디오 메모리를 사용한 더블 버퍼링을 사용할 수 있지만, 조사해본 결과 한가지가 부족했다. 2개의 화면이므로 최소 4개의 메모리가 필요한데, 실제로 그림을 표시하기위해 사용 가능한 메모리는 3개 였다. ㅡ_ㅜ...

 물론 화면 하나는 비디오 메모리를 사용해서 비디오 스위칭을 이용한 더블 버퍼링을 사용하고, 나머지 하나는 메모리 전송을 이용한 더블버퍼링을 사용하면 되지만 윈도우 라이브러리가 복잡해지는 문제가 있어서 둘다 메인 메모리를 이용한 더블 버퍼링을 하기로 결정했다.

참고. NDS 속도에 대한 몇가지 테스트 문서를 참고하면 NDS에서 memcpy, for를 사용한 메모리 전송에 대한 결과를 알 수 있다. 실제 memcpy를 사용하면 7 ~ 8 ms정도가 걸리는데 자작한 고속 전송 함수를 사용하면 4 ms정도에 출력할 수 있다.

 

4.2 메시지 처리(Message Processing) 및 일괄 그리기(Batch Drawing)

 일괄로 그리기란 윈도우가 변경되는 시점에 그래픽 버퍼에 그리는 것이 아니라 변경된 윈도우에 상태를 저장한 뒤에 나중에 일괄적으로 모든 윈도우를 Z-Order의 역순으로 그려나가는 것을 말한다. 이렇게 한 이유는 NDS의 빈약한(??) 프로세싱 능력과 작은 메모리 때문에 윈도우들을 개별 태스크 or 개별 메시지 큐로 구분하지 못했기 때문이다.

 그렇다면 어떻게 멀티 윈도우를 구현한 것일까? 약간의 꼼수를 이용했다. 아래를 보자.

 

4.2.1 메시지 처리(Message Processing) 

 방법은 간단하다. 원래 윈도우 시스템이 개별 윈도우의 메시지 큐에 메시지를 던져줘서 처리하던 방식이라면 내가 만든 윈도우 시스템은 메시지를 Z-Order 순서로 전달하는 방식이라 할 수 있다. 다시 말하면 하나의 메시지를 전체 윈도우가 돌아가면서 사용한다는 말인데, 별도의 처리가 없으면 메시지가 전체에게 전달되므로 동작에 문제가 생긴다.

 따라서 이를 규제하는 방식이 윈도우 영역 및 메시지 종류, 그리고 윈도우 핸들을 이용해서 비교하여 자신의 핸들 or 자신의 윈도우 영역에서 발생한 것이면 메시지의 전달을 더이상 하지 않고 삭제함으로써 무한 메시지 전달을 막았다.

 아래는 이것을 그림으로 나타낸 것이다.

윈도우라이브러리2.PNG

<메시지 처리>

 좌측 상단의 4번 윈도우 자리에 클릭이 일어났을 때 해당 메시지가 어떻게 전달되고 어디서 처리가 완료되는가 하는 그림이다. 메시지는 Z-Order 순서로 전달되다가 4번 윈도우의 처리 루틴에서 자신의 영역에서 발생한 메시지 임을 깨닿고 메시지 처리를 완료한 후 Background 윈도우로 전달하지 않았다.

 

 위와 같은 알고리즘을 사용하기위한 전제조건은 무엇일까?

  • 한가지는 어플리케이션 프로그래머가 OnMessage() 함수에서 적당히 잘 처리한 다음, 메시지를 다른 윈도우에 전달해줘야 한다는 점이다. 위와 같은 상황에서 1번 윈도우에서 메시지를 더이상 뒤로 전달하지 않는다면 다른 윈도우는 메시지를 받지 못하기 때문에 굶주림 상태에 빠진다. 하지만 너무 잘 전달해서 자신의 메시지를 뒤로 전달하면 동작에 문제가 발생한다. 신중한 처리가 필요하다.
  • 다른 한가지는 윈도우는 메시지 처리 루틴에서 너무 시간을 끌면 뒤에 윈도우에게 메시지가 전달되는 시간이 더 걸리기 때문에 시간에 민감한 프로그램을 작성할 때 주의해야 한다는 점이다. 특히 윈도우를 다시 그리거나 하는 작업은 시간이 많이 걸리기 때문에 OnPaint() 같은 함수를 직접 호출해서 버퍼에 다시 그리는 일은 하지 말고 RedrawWindow() 함수를 사용해서 윈도우 매니져가 일괄로 화면을 그리도록 해야 한다.

 

4.2.2 일괄 그리기(Batch Drawing) 

 NDS의 빈약한 처리 능력 때문에 일괄 그리기를 선택했다고 앞서 이야기 했다. 일괄 그리기를 하면 좋은 점이 뭘까?

  • 첫째는 윈도우가 겹겹이 쌓여있을 때 끼어있는 윈도우의 "일부"를 그리는 문제를 간단히 처리할 수 있다. 위의 <메시지 처리>그림에서 2번 윈도우가 갱신되었다면 실제로 그려야하는 영역은 2번 윈도우의 영역에서 1번 윈도우의 영역을 뺀 부분이다. 이 시점에서 버퍼를 바로 갱신하려면 2번 윈도우를 그리고 1번 윈도우를 그 위에 다시 그리면 된다. 이러한 단순한 경우라면 괜찮지만 만약 윈도우가 겹겹이 쌓여있고 다들 일부분이 보이는 상태인데, 그 일부분만 보이는 화면이 계속 갱신되는 상황이라면??? 윈도우 위치에 따른 영역 계산 + 일부분 Draw 등등의 작업을 수행해야 한다. 알고리즘이 점점 복잡해지고 계산량도 점점 늘어난다. 실제로 이러한 방식으로 이전 KKAMAGUI OS를 개발했었는데, 계산량이 많아지다보니 오히려 성능이 그리 좋지 않았다. 그래서 고민한 끝에 일괄 그리기를 이용하여 처리했다.
  •  둘째는 윈도우 메시지를 전달하는 시간을 줄일 수 있다. 현재의 윈도우 라이브러리는 윈도우가 다른 윈도우에게 메시지를 전달하는 구조로 되어있기 때문에, 시간이 많이 걸리는 작업을 메시지 처리 루틴에서 하게되면 치명적이다. 윈도우를 다시 그리는 역할을 윈도우 매니져로 넘김으로써 메시지를 좀 더 빠르게 전달할 수 있고, 윈도우를 계속해서 다시 그리는 일을 하지 않음으로써  응답속도의 향상을 노릴 수 있다.

 

4.3 그리는 영역 설정

 위에서 일괄 그리기의 장점을 이야기 했다. 그렇다고 무턱대고 화면 전체를 다시 그려서 전송하면 효율이 좋을까? 전체 영역 중에 작은 윈도우 일부만 변했는데, 윈도우 전체를 다시 그리는 것은 비효율적이다. 따라서 그리는 영역의 범위를 정하여 해당 범위에 들어있는 윈도우만 다시 그린 후, 그리는 영역이 현재 윈도우 내부에 포함된다면 더이상 그리지 않도록 한다. 이렇게 하면 조금이나마 효율을 높일 수 있다.

 그리는 영역의 설정은 간단한 사각형 더하기로 해결했는데, 알고리즘은 아주 간단하다. 아래의 그림과 같이 각 영역의 Min/Max값을 얻어서 해당 영역 안을 그리도록 했다.

 윈도우라이브러리3.PNG

<그리는 영역 설정>

  위의 그림과 같이 1번 윈도우와 2번 윈도우가 변했다면 두 윈도우의 범위를 Min/Max한 붉은색 사각형이 다시 그려질 영역이 된다. 따라서 이 영역으로 Z-Order를 따라가면서 이 영역을 포함한 윈도우를 찾게되고 해당 윈도우를 그리는 것이다. 위에서 보면 1번 윈도우 및 2번 윈도우 그리고 3번 윈도우가 위 영역에 포함되므로 그려야 된다. 하지만 3번 윈도우 영역이 그리는 영역을 완전히 포함하므로 Background 윈도우는 다시 그릴 필요가 없으므로 3번 윈도우까지만 그린다.

 위와 같이 간단한 알고리즘을 이용해서 다시 그릴 영역을 어느정도 줄일 수 있다. 효율은... 그저 그렇다.. ㅡ_ㅡ;;; 하지만 안하는 것 보다는 좋다.

 

 

 

5.윈도우 관련 클래스 구현

5.1 CWnd 구현 

 CWnd 클래스는 아래와 같이 정의되어있다.

  1. // 기본 윈도우 클래스
    class CWnd
    {
    protected:
        RECT m_stWindowRect;
        bool m_bShow;
        WORD* m_wDisplayAddr;
        char m_vcTitle[ 33 ];
        // 윈도우에 그리기위해 넣음
        CWindowDC m_clWindowDC;
        // ZOrder를 위해 넣음
        CWnd* m_pclZOrderNext;
        CWnd* m_pclParent;
       
        //--------------------------------------------------------------------------
        // 아래는 Window Manager 관련 또는 기타 함수들
        // 쓰는 쪽에서는 아래의 함수를 건드릴 필요 없음
        //--------------------------------------------------------------------------
        void AddWindowToManager( CWnd* pclWnd );
        void DeleteWindowFromManager( CWnd* pclWnd );
        RECT CalcXButtonPosizion( void );
        CWindowManager* GetWindowManagerPtr( void );

  2.  

  3.     //--------------------------------------------------------------------------
        // 아래는 오버로딩할 수 있는 함수 목록
        // 아래의 함수만 상속받은 클래스에서 필요하면 재정의하자.
        //--------------------------------------------------------------------------
        virtual void OnEraseBkgnd( CDC* pclDC );
        virtual bool OnMessage( PMESSAGE pstMessage );
           
       
    public:
        CWnd();
        virtual ~CWnd();
        //--------------------------------------------------------------------------
        // 아래는 윈도우 메니져에서 사용할 용도로 설정된 함수들
        // 상속을 받아서 사용하는 경우라면 쓸 필요 없다.
        //--------------------------------------------------------------------------
        void RedrawZOrderBehindWindow( RECT stRect );
        void SetZOrderNext( CWnd* pclWindow );
        CWnd* GetZOrderNext( void );
       
        //--------------------------------------------------------------------------
        // 아래는 윈도우 사용에 관련된 함수들
        // 상속을 받아서 사용하는 경우라면 쓸 필요 없다.
        //--------------------------------------------------------------------------
        void Create( void* pvAddr, RECT stRect, char* pcName, bool bShow,
                CWnd* pclParent );
        void SetDisplayAddr( void* pvAddr );
        void* GetDisplayAddr( void );
       
        bool MoveWindow( int iX1, int iY1, int iX2, int iY2 );
        bool MoveWindow( RECT stRect );
        void ShowWindow( bool bShow );
        void RedrawWindow( void );
       
        void SetWindowText( char* pcTitle );
        char* GetWindowText( void );
        RECT GetWindowRect( void );
        RECT GetClientRect( void );
       
        void SetActiveWindow( void );
       
        void SetParent( CWnd* pclParent );
        CWnd* GetParent( void );
       
        void ScreenToClient( PPOINT pstPoint );
        void ScreenToClient( PRECT pstRect );
        void ClientToScreen( PPOINT pstPoint );
        void ClientToScreen( PRECT pstRect );
       
        void DestroyWindow( void );
        bool IsInWindowRect( int iX, int iY );
        bool IsInWindowRect( RECT stRect );
        bool IsInXButtonRect( int iX, int iY );
        bool IsInTitleBarRect( int iX, int iY );
        bool IsXButtonDownMessage( PMESSAGE pstMessage );
       
        bool DispatchMessage( PMESSAGE pstMessage );
        bool SendMessage( WORD wType, WPARAM wParam, LPARAM lParam );
        bool IsBelongMainWindowManager( void );
        bool IsBelongSubWindowManager( void );
        void GetXYFromMessage( PMESSAGE pstMessage, int* piX, int* piY );

  4.  

  5.     //--------------------------------------------------------------------------
        // 아래는 오버로딩할 수 있는 함수 목록
        // 아래의 함수만 상속받은 클래스에서 필요하면 재정의하자.
        //--------------------------------------------------------------------------
        virtual void OnPaint( void );
        virtual void OnDestroy( void );
        virtual void DrawXButton( CDC* pclDC );
        virtual void DrawTitleBar( CDC* pclDC );
    };

 최대한 MFC 클래스와 비슷하게 구현하려 노력했다. 낯익은 함수들이 반가울 것이다. CWnd 클래스는 모든 윈도우의 기본이 되는 윈도우로써 윈도우 기본 프레임을 그려주는 역할과 윈도우 메니져의 일부로써 자료구조 역할을 수행한다. 하지만 사실 거의 Draw 쪽 함수만 처리가 되어있을 뿐, 메시지 처리라든지 윈도우 이동에 관한 부분은 거의 가지고 있지 않다.

 정말 "기본" 적인 윈도우 구성에 대한 정보만 가지고 있다. 

 

5.2 CSkinWnd 구현 

 CSkinWnd는 그래픽 파일을 넣어주면 해당 그래픽 파일을 BitBlt 해서 윈도우에 스킨을 씌우는 프로그램이다. 스킨을 씌우는 방법은 향후 Library Tutorial을 다루면서 설명할 것이다. 그래픽 포맷인 KNG(KKAMAGUI NDS Graphic)에 대해서는 추후에 다룰 예정이다. 뭐 사실 RGB555 포맷으로 바꿔주는 것 밖에는 없다.

  1. class CSkinWnd : public CWnd
    {
    private:
        BYTE* m_pbKNGBuffer;
     
    public:
        CSkinWnd( void );
        ~CSkinWnd( void );
  2.     void SetKNGSkin( BYTE* pbKngBuffer );   
    protected:
        void OnPaint( void );
    }; 
  3. ... 구현 ... 
  4. /**
        화면 그리기 처리
            배경 그림을 그려준다.
    */
    void CSkinWnd::OnPaint( void )
    {
        CBitmap clBitmap;
        CDC clBitmapDC;
        CWindowDC clWindowDC( this );
        RECT stRect;
  5.     if( m_pbKNGBuffer == NULL )
        {
            return ;
        }
       
        // Bitmap 클래스에 KNG를 로드한 후 윈도우 전체에 bitblt 한다.
        clBitmap.LoadKNG( m_pbKNGBuffer );
        clBitmapDC.SelectObject( &clBitmap );
        stRect = GetWindowRect();
  6.     clWindowDC.BitBlt( 0, 0, GETWIDTH( stRect ), GETHEIGHT( stRect ),
            &clBitmapDC, 0, 0, 0 );

  윈도우 프로그래밍할 때 다이얼로그에 스킨을 씌워본 사람이라면 낯이 익은 코드일 것이다. 그래픽 포맷과 이미지 크기의 제한만 제외하면 거의 윈도우 프로그램하는 것과 같다.

 

5.3 CListWnd 구현 

 CListWnd는 윈도우의 List Box와 비슷한 기능을 하는 클래스다. 각 라인에 항목을 표시하고 선택된 항목을 표시해주는 기능을 한다. 부가적으로 Call Back을 지원하여 그려질 라인별로 그리는 방식에 대한 처리나 선택되었을 때 처리에 대한 구현도 표함하고 있다. CListWnd 자체는 Text String 기반으로 동작하지만, 구조체와 같은 정보를 삽입하고 Call Back만 잘 처리해 준다면 기타 여러가지 자료 구조도 표시할 수 있다. 이 부분에 대해서는 홈브루 중에 File Explorer를 참고하면 된다.

  1. class CListWnd : public CWnd
    {
    protected:
        CList m_clContentList;
  2.     int m_iTouchX;
        int m_iTouchY;
        int m_iLastX;
        int m_iLastY;
        int m_iViewItemNumber;
        bool m_bSelectMode;
        bool m_bLButtonDown;
  3. private:
        bool ProcessScrollMode( PMESSAGE pstMessage );
        bool ProcessSelectMode( PMESSAGE pstMessage );
        bool ProcessTitleBarMessage( int iX, int iY  );
       
    public:
        CListWnd( void );
        ~CListWnd( void );
  4.     bool AddItem( void* pvContent );
        void DeleteAllItem( void );
        bool DeleteItem( void* pvContent );
        int GetItemCount( void );
       
        void* GetItem( int iIndex );
        int GetSelectedItemIndex( void );
        bool IsSelectionMode( void );
        int GetMaxVisibleLineCount( void );
        void SetSelectionMode( bool bSelectMode );
        void SetFirstVisibleItem( int iIndex );
       
    protected:
        void OnPaint( void );
        bool OnMessage( PMESSAGE pstMessage );
  5.     //--------------------------------------------------------------------------
        // 리스트에 Item을 다른 방식으로 표시하고 싶으면 아래의 함수를 오버로딩해서
        // 원하는 대로 그리면 된다.
        //  한 라인 라인을 그릴때 호출되는 Call back 함수
        //--------------------------------------------------------------------------
        virtual void RenderContent( RECT stDrawRect, CDC* pclDC, void* pvContent,
                int iItemIndex );
       
        // 리스트의 Item이 선택되었을 때 호출되는 함수
        virtual void OnSelectCursorDown( void );
        virtual void OnSelectCursorUp( void );
    }; 

 

5.4 CChildWnd 구현 

 CChildWnd는 CWnd와 같으나 다른 점은 Window Manager에 등록이 되지 않기 때문에, 메시지에 대한 처리는 부모 윈도우가 Child Window에 넣어줘야 한다는 점이다. 이렇게 하면 편리한 점은 자식 윈도우를 부모 윈도우에 다 맡김으로써 Window Manager에 로드를 줄일 수 있고 Z-Order의 처리가 아주 간단하기 때문이다.

  1. // Child Wnd는 윈도우 매니저에 속하지 않는다는 점 말고는 CWnd와 동일하다.
    class CChildWnd : public CWnd
    {
    private:
        BYTE* m_pbKNGBuffer;
       
    public:
        CChildWnd( void );
        ~CChildWnd( void );
        
        // Override된 함수들
        void AddWindowToManager( CWnd* pclWnd );
        void DeleteWindowFromManager( CWnd* pclWnd );
    }; 

 

5.5 CDC 구현 

 CDC는 윈도우의 대표적인 그래픽 관련 클래스이다. CPaintDC와 CWindowDC도 구현되어있으며, 차의점은 CPaintDC 같은 경우 Title Bar의 높이를 자동으로 빼서 계산해 준다는 점이다.

  1. // 윈도우 관련 DC를 처리하는 Base Class
    // 하는일 없음
    class CDC
    {
    protected:
     CWnd* m_pclWindow;
     CBitmap* m_pclBitmap;
     int m_iLastX;
     int m_iLastY;
     int m_iTitleHeight;
     
    protected:
     void DrawRect( PRECT pstRect, WORD wColor, bool bFill );
     WORD* GetBufferAddr();
     RECT GetBufferRect();
     
    public:
     CDC();
     ~CDC();
     
     void SetWindow( CWnd* pclWindow );
     void SelectObject( CBitmap* pclBitmap );
     
     bool MoveTo( int iX, int iY );
     bool LineTo( int iX, int iY, WORD wColor );
     
     void FillRect( PRECT pstRect, WORD wColor );
     void FillRect( int iX1, int iY1, int iX2, int iY2, WORD wColor );
     void FrameRect( PRECT pstRect, WORD wColor );
     void FrameRect( int iX1, int iY1, int iX2, int iY2, WORD wColor );
     void TextOut( int iX, int iY, char* pcString, WORD wForeColor,
             WORD wBackColor );
     void BitBlt( int iDstX, int iDstY, int iWidth, int iHeight,
      CDC* pclDC, int iSrcX, int iSrcY, int iMode );
     void SetPixel( PPOINT pstPoint, WORD wColor );
    };
  2. // Paint DC
    class CPaintDC : public CDC
    {
    public:
     CPaintDC( CWnd* pclWindow );
     ~CPaintDC();
    };
  3. // Window DC
    class CWindowDC : public CDC
    {
    public:
     CWindowDC();
     CWindowDC( CWnd* pclWindow );
     ~CWindowDC();
    }; 

 

5.6 CBitmap 구현 

 CBitmap은 간단히 Bitmap 파일을 관리하는 클래스로 단순한 버퍼 역할을 한다. 지금은 KNG 파일 포맷을 사용하도록 되어있는데 아래에 KNG 구조체의 정보를 앞에 포함하고 내부 데이터는 RGB555 포맷을 사용하는 아주 간단한 구조의 파일이다.

  1. #define KNG_SIGNATURE "KNG"
  2. // KKAMAGUI NDS GRAPHIC 파일의 헤더 구조체
    typedef struct kkamaguiNdsGraphicStruct
    {
        char vcSignature[ 3 ]; // KNG 설정
        BYTE bVersion;         // 이미지 포맷 버전, 0x00 설정
        int iWidth;            // 이미지 넓이
        int iHeight;           // 이미지 높이
    } KNG, * PKNG;
  3. // Bitmap 관련 클래스
    class CBitmap
    {
    private:
        WORD* m_pwBitmap;
        SIZE m_stSize;
       
    public:
        CBitmap();
        ~CBitmap();
        
        WORD* GetBufferAddr();
        RECT GetBitmapRect();
       
        bool LoadKNG( BYTE* pbKNG );
    };

 

5.7 비디오 메모리 고속 전송 구현

 ARM 레지스터를 최대한 사용해서 전송하는 방식을 사용한다. 나름 빠르다.

  1. //  Frame Buffer 통짜를 빠르게 LCD로 복사해 주는 함수
    //      256 * 192 * 2 = 98304Byte
    //      r12 => Loop Count r2~r11 => 40Byte
    //      98304Byte = 40 * 2457(Loop Count) + 24(Remain)
    FastFrameBufferDump:
        push { r4 - r12 }
        // 2457 값을 바로 넣을 수 없어서 쉬프트 연산으로 넣는다.
        mov r4, #153
        mov r5, #9
        add r12, r5, r4, LSL #4
       
        // 40Byte Block씩 전송한다.
        Loop:
            ldmia r1!, { r2 - r11 }
            stmia r0!, { r2 - r11 }
            subs r12, #0x01
            bne Loop   
       
        // 남은 24Byte를 전송한다.
        ldmia r1, { r2 - r7 }
        stmia r0, { r2 - r7 }
           
        pop { r4 - r12 }
        bx lr

 

6.Graphic 기본 함수들 

6.1 Line 함수 

 Line 함수는 Bresenham 알고리즘으로 그리도록 되어있다. 물론 클립핑 부분은 여기서 하지 않고 따로 cohen-sutherland 알고리즘을 이용한다.

  1. //=============================================================================
    //
    //  cohen-sutherland 알고리즘을 이용한 클립핑
    //
    //=============================================================================
    #define LEFT_EDGE 0x01
    #define RIGHT_EDGE 0x02
    #define BOTTOM_EDGE 0x04
    #define TOP_EDGE 0x08
  2. //0000은 중앙
    #define INSIDE(a) ( !a )
    #define REJECT( a, b ) ( a & b )
    #define ACCEPT( a, b ) ( !( a | b ) )
  3. /**
        좌표가 MIN/MAX의 밖인지 안인지 판단
    */
    BYTE Encode( RECT stClippingRect, POINT stPt )
    {
        BYTE bCode;
       
        bCode = 0;
       
        if( stPt.iX < stClippingRect.iX1 )
        {
            bCode = bCode | LEFT_EDGE;
        }
        if( stPt.iX > stClippingRect.iX2 )
        {
            bCode = bCode | RIGHT_EDGE;
        }
        if( stPt.iY < stClippingRect.iY1 )
        {
            bCode = bCode | TOP_EDGE;
        }
        if( stPt.iY > stClippingRect.iY2 )
        {
            bCode = bCode | BOTTOM_EDGE;
        }
        return bCode;
    }
  4. /**
        두점을 교환
    */
    void SwapPoints( POINT* pstP1, POINT* pstP2 )
    {
        POINT stTemp;
       
        stTemp = *pstP1;
        *pstP1 = *pstP2;
        *pstP2 = stTemp;
    }
  5. /**
        두 속성을 교환
    */
    void SwapCodes( BYTE* bC1, BYTE* bC2 )
    {
        BYTE bTemp;
       
        bTemp = *bC1;
        *bC1 = *bC2;
        *bC2 = bTemp;
    }
  6. /**
        Line을 클립핑한다.
    */
    void ClipLines( RECT stClippingRect, void* pvAddr, POINT stP1, POINT stP2,
            WORD wColor )
    {
        BYTE bCode1, bCode2;
        int iDone = FALSE, iDraw = FALSE;
        float m;
  7.     if( stP2.iX != stP1.iX )//수직선이 아닐때
        {
            m = ( float ) ( stP2.iY - stP1.iY ) / ( stP2.iX - stP1.iX );
        }
        else
        {
            m = 0;
        }
       
        while( !iDone )
        {
            bCode1 = Encode( stClippingRect, stP1 );
            bCode2 = Encode( stClippingRect, stP2 );
           
            if( ACCEPT( bCode1, bCode2 ) )
            {
                //두점 다 안에 있으면 끝
                iDone = TRUE;
                iDraw = TRUE;
            }
            else
            {
                if( REJECT( bCode1, bCode2 ) )
                {
                    //두점다 화면 밖이면 끝
                    iDone = TRUE;
                }
                else
                {
                    // 만약 점 1이 안에 있으면 점 2를 사용한다.
                    if( INSIDE( bCode1 ) )
                    {
                        //p1이 중앙에 있으면 p2와 교환
                        SwapPoints( &stP1, &stP2 );
                        SwapCodes( &bCode1, &bCode2 );
                    }
                   
                    if( bCode1 & LEFT_EDGE )
                    {
                        stP1.iY += ( int ) ( ( stClippingRect.iX1 - stP1.iX ) * m );
                        stP1.iX = stClippingRect.iX1;
                    }
                    else if( bCode1 & RIGHT_EDGE )
                    {
                        stP1.iY += ( int ) ( ( stClippingRect.iX2 - stP1.iX ) * m );
                        stP1.iX = stClippingRect.iX2;
                    }
                    else if( bCode1 & TOP_EDGE )
                    {
                        if( stP2.iX != stP1.iX )//수직선이 아닐때
                        {
                            stP1.iX += ( int ) ( ( stClippingRect.iY1 - stP1.iY ) /
                                    m );
                        }
                        stP1.iY = stClippingRect.iY1;
                    }
                    else if( bCode1 & BOTTOM_EDGE )
                    {
                        if( stP2.iX != stP1.iX )//수직선이 아닐때
                        {
                            stP1.iX += ( int ) ( ( stClippingRect.iY2 - stP1.iY ) /
                                    m );
                        }
                        stP1.iY = stClippingRect.iY2;
                    }
                }
            }
           
            if( iDraw )
            {
                //선그리기
                BresenhamLine( pvAddr, stP1.iX, stP1.iY, stP2.iX, stP2.iY, wColor );
            }
        }
    }
  8. //=============================================================================
    //
    //  Bresenham 알고리즘을 이용한 클립핑
    //
    //=============================================================================
  9. /**
        라인을 그린다.
    */
    void BresenhamLine( void* pvAddr, int iX1, int iY1, int iX2, int iY2,
        unsigned short usColor )
    {
        int dy = iY2 - iY1;
        int dx = iX2 - iX1;
        int stepx, stepy;
        unsigned short* pusStartAddr = ( unsigned short* ) pvAddr;
       
        if( dy < 0 )
        {                                 // 기울기를 양수처리함
            dy = -dy;
            stepy = -1;
        }
        else
        {
            stepy = 1;
        }
     
        if( dx < 0 )
        {
            dx = -dx;
            stepx = -1;
        }
        else
        {
            stepx = 1;
        }
     
        dy <<= 1;                                     // dy*2 와 같은 의미(비트연산)
        dx <<= 1;                                     // dx*2 와 같은 의미(비트연산)
       
        pusStartAddr[ iX1 + SCREEN_WIDTH * iY1 ] = usColor;
        if( dx > dy )
        {
            int fraction = dy - (dx >> 1);     // dx>>1 은 dx/2와 같은 의미(비트연산)
            while( iX1 != iX2 )
            {
                if (fraction >= 0)
                {
                    iY1 += stepy;
                    fraction -= dx;                 // fraction -= 2*dx 과 같은 의미
                }
                iX1 += stepx;
                fraction += dy;                     // fraction -= 2*dy 과 같은 의미
               
                pusStartAddr[ iX1 + SCREEN_WIDTH * iY1 ] = usColor;
            }
        }
        else
        {
            int fraction = dx - (dy >> 1);
            while (iY1 != iY2)
            {
                if (fraction >= 0)
                {
                    iX1 += stepx;
                    fraction -= dy;
                }
                iY1 += stepy;
                fraction += dx;     
               
                pusStartAddr[ iX1 + SCREEN_WIDTH * iY1 ] = usColor;
            }
        }

 

6.2 Box 함수 

 Box 함수는 속이 찬 사각형과 빈 사각형 두가지 타입이 있다.

  1. /**
        Box를 그린다.
    */
    void DrawBox( RECT stClippingRect, void* pvAddr, int iX1, int iY1, int iX2,
            int iY2, unsigned short usColor, bool bFill )
    {
        int j;
        int iTemp;
        int iCount;
        WORD* pwAddr = ( WORD* ) pvAddr;
        WORD* pwTemp;
       
        // 속을 체우게 되어있으면 영역을 체운다.
        if( bFill == true )
        {
            // 영역 클립핑을 한다.
            MinMaxClipping( stClippingRect, &iX1, &iY1 );
            MinMaxClipping( stClippingRect, &iX2, &iY2 );
            iCount = iX2 - iX1 + 1;
            iTemp = iY1 * SCREEN_WIDTH + iX1;
           
            for( j = iY2 - iY1 ; j > 0 ; j-- )
            {
                pwTemp = pwAddr + iTemp;
                FastWordMemSet( pwTemp, usColor, iCount );
                iTemp += SCREEN_WIDTH;
            }
        }
        // 안 채우게 되어있으면 라인만 그린다. 클립핑은 DrawLine에서 해준다.
        else
        {
            DrawLine( stClippingRect, pvAddr, iX1, iY1, iX2, iY1, usColor );
            DrawLine( stClippingRect, pvAddr, iX1, iY2, iX2, iY2, usColor );
            DrawLine( stClippingRect, pvAddr, iX1, iY1, iX1, iY2, usColor );
            DrawLine( stClippingRect, pvAddr, iX2, iY1, iX2, iY2, usColor );
        }

 

6.3 Pixel 함수 

 Pixel 함수는 해당 위치에 RGB555 포맷으로 점을 찍는 역할을 한다. 

  1. /**
     점을 찍는다.
    */
    void DrawPixel( RECT stClippingRect, void* pvAddr, int iX, int iY,
            unsigned short usColor )
    {
        // 화면을 벗어나면 찍지 않는다.
        if( IsInRange( stClippingRect, iX, iY ) == false )
        {
            return ;
        }
       
     *( ( unsigned short* ) pvAddr + ( iY * SCREEN_WIDTH ) + iX ) = usColor;

 

6.4 한글/영문 출력 

 한글/영문 출력 부분은 01 NDS 한글 출력 라이브러리 를 참고하자.

 

7.설치 

7.1 Include 설치 

 devkitPro가 설치된 폴더에 libnds/include 폴더를 찾아서 하위에 window 폴더를 생성한뒤 첨부에 있는 include.zip 파일을 해제한다.

include_설치.PNG

<include 설치>

 

7.2 Library 설치 

 devkitPro가 설치된 폴더에 libnds/lib 폴더를 찾아서 libwindow.a 파일을 복사한다.

 

8.마치면서... 

 이상으로 윈도우 라이브러리 구성 및 구현 방법, 그리고 설치 방법까지 알아보았다. 실제 사용방법은 추후의 Tutorial을 통해 알아보도록 하자. 아래는  NDS 윈도우 시스템을 이용해서 만들어진 홈브루의 실제 화면이다.

화면5.PNG 화면1.PNG 화면2.PNG 화면3.PNG 화면4.PNG

 

9.첨부

 

 

 

이 글은 스프링노트에서 작성되었습니다.

02 NDS Hardware Spec And Memory Map

원문 : http://kkamagui.springnote.com/pages/409612

 

들어가기 전에...

 

1. 하드웨어 사양(Hardware Spec)

 

NDS에 대해서 Wikipedia에 검색을 해봤더니 아래와 같은 정보를 얻을 수 있었다(정말 대단하다 wikipedia~~!!).

Technical specifications

  • Mass: 275 grams (9.7 ounces).
  • Physical size: 148.7 x 84.7 x 28.9 mm (5.85 x 3.33 x 1.13 inches).
  • Screens: Two separate 3-inch TFT LCD, resolution of 256 x 192 pixels, dimensions of 62 x 46 mm and 77 mm diagonal, and a dot pitch of 0.24 mm. Note The gap between the screens is approximately 21mm, equivalent to about 92 "hidden" lines. The lowermost display of the Nintendo DS is overlaid with a resistive touch screen, which registers pressure from one point on the screen at a time, averaging multiple points of contact if necessary.
  • CPUs: Two ARM processors, an ARM946E-S main CPU and ARM7TDMI co-processor at clock speeds of 67 MHz and 33 MHz respectively, with 4 MB of main memory which requires 1.65 volts.
  • Card size
    • Data size: Up to 2 gigabit ( = 2048 Mb or 256 MB).
    • Physical size: 33.0 × 35.0 × 3.8 mm
    • Weight: About 4 grams

The system's 3D hardware performs transform and lighting, texture-coordinate transformation, texture mapping, alpha blending, anti-aliasing, cel shading and z-buffering. However, it uses Point (nearest neighbor) texture filtering, leading to some titles having a blocky appearance. The system is theoretically capable of rendering 120,000 triangles per second at 30 frames per second. Unlike most 3D hardware, it has a limit on the number of triangles it can render as part of a single scene; this limit is somewhere in the region of 4000 triangles. The 3D hardware is designed to render to a single screen at a time, so rendering 3D to both screens is difficult and decreases performance significantly.

The system has two 2D engines, one per screen. These are similar to (but more powerful than) the Game Boy Advance's 2D engine.

Games use a proprietary solid state ROM "Game Card" format resembling the memory cards used in other portable electronic devices such as digital cameras. It currently supports cards up to 2 gigabit[15] in size. The cards always have a small amount of flash memory or an EEPROM to save user data, for example progress in a game or high scores. The game cards are 33.0 × 35.0 × 3.8 mm, and weigh around 3.5 grams (1/8 ounces).

The unit has compatibility with Wi-Fi, and a special wireless format created by Nintendo and secured using RSA security signing (used by the wireless drawing and chatting program PictoChat for the DS). Wi-fi is used for accessing the Nintendo Wi-Fi Connection, where users can use the internet or compete with other users playing the same Wi-Fi compatible game.

 

  위 글을 간단히 요약하면 아래와 같다.

  • LCD : TFT-LCD 256 x 192 pixels
  • CPU :  ARM946E-S main CPU 67MHz. ARM7TDMI co-processor 33 MHz
  • RAM : 4Mbyte
  • Graphic : 3D/2D Engine
  • Input : Key and Touch Pad
  • Communication : Wi-Fi

 

 무엇보다 코어가 듀얼이라서 마음에 든다. @0@)/~ 램이 조금 부족한것 같은데... 어쩔 수 없으니... ㅜ_ㅜ

 아래는 전체적인 구성도이다. http://www.dev-scene.com/NDS/Tutorials_Day_2에서 찾아볼 수 있다.

 

Dov_DS_MemoryMap.png

 

2. 메모리맵(Memory Map)

2.1 전체 구성

 http://www.bottledlight.com/ds/index.php/Memory/Layout 와 http://nocash.emubase.de/gbatek.htm#dsmemorymaps를 참고하면 자세한 메모리 맵의 내용을 볼 수 있다.

 NDS Wiki Tech의 내용을 보면 아래와 같다.

ARM9

Name Region base Size Mirrored Width / modes
ITCM 0x00000000* 16 KB no 32 / all
DTCM 0x00800000* 16 KB no 32 / all
Main RAM 0x02000000 4 MB (8 MB) yes 16 / all
Shared RAM 0x03000000 32 KB yes 32 / 16,32
Registers 0x04000000 * * *
Palette RAM 0x05000000 2 KB yes 16 / 16,32
Video RAM 0x06000000 ? ? 16 / 16,32
Sprite RAM 0x07000000 2 KB yes 16 / 16,32
GBA cart ROM 0x08000000 32 MB no 16 / all
GBA cart RAM 0x0A000000 64 KB yes 8 / 8
BIOS (ARM9) 0xFFFF0000 4 KB no unknown / all

 

ARM7

 

Name Region base End Size Mirrored Width / modes
BIOS (ARM7) 0x00000000 0x00003FFF 16 KB no unknown
Main RAM 0x02000000 0x023FFFFF 4 MB (8 MB) yes 16 / all
Shared RAM 0x037F8000 0x037FFFFF 32 KB ? 32 / 16,32
Private RAM 0x03800000 0x0380FFFF 64 KB yes 32 / 16,32
Registers 0x04000000 * * * *
Wifi Control 0x04800000 0x04800FFF * yes** 16
Wifi MAC memory 0x04804000 0x04805FFF 8 KB yes** 16
GBA cart ROM 0x08000000 0x09FFFFFF 32 MB no 16 / all
GBA cart RAM 0x0A000000 0x0A00FFFF 64 KB yes 8 / 8

The ARM7 BIOS is protected via a PC check. The portion below PROTECTION_CR? can be read when PC < PROTECTION_CR?, and the portion beyond it can be read when PC < 0x4000.

 

The Main RAM is always available to both processors, although one has priority over it, and the other will be delayed if both try to access it at the same time.

The 'shared' RAM actually consists of two banks of 16 KB each, and either one only available to a single processor at a time. They can be switched back and forth, to implement a buffer passing scheme for e.g. wireless packets or a sound buffer.

ITCM and DTCM can be relocated using the system control coprocessor CP15

The ARM9 BIOS provides a handful of functions, but does little in the way of system setup. It clears some memory, then waits for the ARM7 bios to signal that system init is complete.

 ITCM과 DTCM 같은 경우는 조금 특별하므로 co-processor 쪽을 봐야 한다(cp15). 좀더 상세한 내용은 아래의 TCM 쪽에서 살펴보자.

 위의 내용 중에 중요한 부분은 메인 메모리 같은 경우 두 프로세스에서 항상 사용 가능하지만 동시에 접근하면 하나가 먼저 작업하고 다른 하나는 지연된다는 것이다.

 또 다른 중요한 부분은 Shared RAM 같은 경우는 두 프로세스 중에 하나만 접근 가능하고 이것을 스위칭을 통해 다른 프로세스에게 넘겨주는 방식으로 동작하는 것이다. 즉 한쪽이 데이터를 체운 후 다른 쪽이 받아서 처리하는 중계 역할인 버퍼로 사용될 수 있다.

 

 Video RAM영역 같은 경우는 자세하게 나와있지 않는데, 이부분은 GBA Tech의 문서를 보면 아래와 같이 잘 나와있다. http://nocash.emubase.de/gbatek.htm#dsmemorymaps에 내용들이다.

 ARM9

00000000h  Instruction TCM (32KB) (not moveable) (mirror-able to 1000000h)
  0xxxx000h  Data TCM        (16KB) (moveable)
  02000000h  Main Memory     (4MB)
  03000000h  Shared WRAM     (0KB, 16KB, or 32KB can be allocated to ARM9)
  04000000h  ARM9-I/O Ports
  05000000h  Standard Palettes (2KB) (Engine A BG/OBJ, Engine B BG/OBJ)
  06000000h  VRAM - Engine A, BG VRAM  (max 512KB)
  06200000h  VRAM - Engine B, BG VRAM  (max 128KB)
  06400000h  VRAM - Engine A, OBJ VRAM (max 256KB)
  06600000h  VRAM - Engine B, OBJ VRAM (max 128KB)
  06800000h  VRAM - "LCDC"-allocated (max 656KB)
  07000000h  OAM (2KB) (Engine A, Engine B)
  08000000h  GBA Slot ROM (max. 32MB)
  0A000000h  GBA Slot RAM (max. 64KB)
  FFFF0000h  ARM9-BIOS (32KB) (only 3K used)
The ARM9 Exception Vectors are located at FFFF0000h. The IRQ handler redirects to [DTCM+3FFCh].

ARM7
00000000h  ARM7-BIOS (16KB)
  02000000h  Main Memory (4MB)
  03000000h  Shared WRAM (0KB, 16KB, or 32KB can be allocated to ARM7)
  03800000h  ARM7-WRAM (64KB)
  04000000h  ARM7-I/O Ports
  04800000h  Wireless Communications Wait State 0
  04808000h  Wireless Communications Wait State 1
  06000000h  VRAM allocated as Work RAM to ARM7 (max. 256K)
  08000000h  GBA Slot ROM (max. 32MB)
  0A000000h  GBA Slot RAM (max. 64KB)
The ARM7 Exception Vectors are located at 00000000h. The IRQ handler redirects to [3FFFFFCh aka 380FFFCh].

Further Memory (not mapped to ARM9/ARM7 bus)
3D Engine Polygon RAM (52KBx2)
  3D Engine Vertex RAM (72KBx2)
  Firmware (256KB) (built-in serial flash memory)
  GBA-BIOS (16KB) (not used in NDS mode)
  NDS Slot ROM (serial 8bit-bus, max. 4GB with default protocol)
  NDS Slot EEPROM (serial 1bit-bus)

Shared-RAM
Even though Shared WRAM begins at 3000000h, programs are commonly using mirrors at 37F8000h (both ARM9 and ARM7). At the ARM7-side, this allows to use 32K Shared WRAM and 64K ARM7-WRAM as a continous 96K RAM block

사실 Video Memory는 A,B,C,D 외에도 E, F, G, H, I가 있다. 자세한 내용은 03 비디오 모드 제어(Video Mode Control)에서 볼 수 있다.

 

  음~ 갑자기 난데없이 WRAM 이란 용어가 나왔다. 역시나 궁금하니 wikipedia에 물었다.

Window RAM (WRAM)

Window RAM or WRAM is an obsolete type of semiconductor computer memory that was designed to replace video RAM (VRAM) in graphics adapters. It was developed by Samsung and also marketed by Micron Technology, but had only a short market life before being superseded by SDRAM and SGRAM.

WRAM has a dual-ported dynamic RAM structure similar to that of VRAM, with one parallel port and one serial port, but has extra features to enable fast block copies and block fills (so-called window operations). It was often clocked at 50 MHz. It has a 32-bit wide host port to enable optimal data transfer in PCI and VESA Local Bus systems. Typically WRAM was 50% faster than VRAM, but with costs 20% lower. It is sometimes erroneously called Windows RAM, because of confusion with the Microsoft Windows operating systems, to which it is unrelated apart from the fact that window operations could boost the performance of windowing systems.

It was used by Matrox on both their MGA Millennium and Millennium II graphics cards.

 비디오 메모리보다는 빠르고 싸다는데... 데이터 메모리 처럼 쓸 수 있나보다. 어쨋든 추가로 활용할 수 있는 메모리가 많다는 것은 좋은 거니깐 그리 알고 넘어가자.

 

 메모리에 대한 추가 정보는 http://neimod.com/dstek/ 에서 찾아볼 수 있다. 중복된 정보 말고 참고할만한 정보를 추리면 아래와 같다.

Memory mirroring

Every memory section, e.g. main memory, shared iwram, io, vram, palette, etc. are mirrored.
For example, Main Memory from 0200:0000-023F:FFFF is mirrored in 0240:0000-027F:FFFF, 0280:0000-02BF:FFFF, etc. until 02FF:FFFF
An other example, Shared IWRAM from 037F:8000-037F:FFFF, is mirrored from 0300:0000 to 037F:7FFF.
This is for each memory section, although it is sometimes difficult to tell where the mirroring starts and stops, ie. IO.

Shared IWRAM

The name shared IWRAM (consists of 2 16kb blocks) can be misleading, as only one CPU has access to it.
However, using WRAMCNT each 16k block can be assigned to a specific CPU.
When processing is done for example, the block can be assigned to the other CPU quickly for processing, without copying it via main memory or the IPC fifo.

Main Memory

Main memory, consisting of one big block of 4MB memory, can be accessed by both CPU's.
However, only one CPU can read/write/execute from it at a time. When both CPUs are trying to read main memory, one will have priority over the other.
See EXMEMCNT for more information.

 Shared RAM 영역은 WRAMCNT 레지스터를 통해 어느 CPU에서 사용가능한지 제어가 가능한 것 같다. 추후 사용해 보도록 하자.

 

2.2 ITCM, DTCM

 TCM에 대한 자세한 내용은 http://nocash.emubase.de/gbatek.htm#dsmemorycontrolcacheandtcm에서 찾아볼 수 있다. TCM은 ARM9에만 연결되어있으며 아래와 같다.

TCM and Cache are controlled by the System Control Coprocessor,
ARM CP15 System Control Coprocessor

The specifications for the NDS9 are:

Tightly Coupled Memory (TCM)

ITCM 32K, base=00000000h (fixed, not move-able)
  DTCM 16K, base=moveable  (default base=27C0000h)
Note: Although ITCM is NOT moveable, the NDS Firmware configures the ITCM size to 32MB, and so, produces ITCM mirrors at 0..1FFFFFFh. Furthermore, the PU can be used to lock/unlock memory in that region. That trick allows to move ITCM anywhere within the lower 32MB of memory.

Cache
Data Cache 4KB, Instruction Cache 8KB
  4-way set associative method
  Cache line 8 words (32 bytes)
  Read-allocate method (ie. writes are not allocating cache lines)
  Round-robin and Pseudo-random replacement algorithms selectable
  Cache Lockdown, Instruction Prefetch, Data Preload
  Data write-through and write-back modes selectable


Protection Unit (PU)
Recommended/default settings are:

 

Region  Name            Address   Size   Cache WBuf Code Data
  -       Background      00000000h 4GB    -     -    -    -
  0       I/O and VRAM    04000000h 64MB   -     -    R/W  R/W
  1       Main Memory     02000000h 4MB    On    On   R/W  R/W
  2       ARM7-dedicated  027C0000h 256KB  -     -    -    -
  3       GBA Slot        08000000h 128MB  -     -    -    R/W
  4       DTCM            027C0000h 16KB   -     -    -    R/W <= devkit ARM에서는 주소를 0x0B000000로 재설정해서 사용
  5       ITCM            01000000h 32KB   -     -    R/W  R/W
  6       BIOS            FFFF0000h 32KB   On    -    R    R
  7       Shared Work     027FF000h 4KB    -     -    -    R/W
위의 내용중에 DTCM영역에 대해서 잠깐 부연 설명을 하자면, 위의 값은 default로 설정되는 값인데 실제로 devkitARM의 crt0.S 파일을 살펴보면 위 영역을 다시 0xB000000 영역으로 재정의해서 사용함을 알 수 있다. 홈브루 파일을 보면서 헷갈리지 말자.
 
Notes: In Nintendos hardware-debugger, Main Memory is expanded to 8MB (for that reason, some addresses are at 27NN000h instead 23NN000h) (some of the extra memory is reserved for the debugger, some can be used for game development). Region 2 and 7 are not understood? GBA Slot should be max 32MB+64KB, rounded up to 64MB, no idea why it is 128MB?
 
DTCM and ITCM do not use Cache and Write-Buffer because TCM is fast. Above settings do not allow to access Shared Memory at 37F8000h? Do not use cache/wbuf for I/O, doing so might suppress writes, and/or might read outdated values.
The main purpose of the Protection Unit is debugging, a major problem with GBA programs have been faulty accesses to memory address 00000000h and up (due to [base+offset] addressing with uninitialized (zero) base values). This problem has been fixed in the NDS, for the ARM9 processor at least, still there are various leaks: For example, the 64MB I/O and VRAM area contains only ca. 660KB valid addresses, and the ARM7 probably doesn't have a Protection Unit at all. Alltogether, the protection is better than in GBA, but it's still pretty crude compared with software debugging tools.
Region address/size are unified (same for code and data), however, cachabilty and access rights are non-unified (and may be separately defined for code and data).

Note: The NDS7 doesn't have any TCM, Cache, or CP15. 

 홈브루를 컴파일 하는 과정을 살펴보면( 00 NDS makefile 및 NDS 파일 생성 과정 분석 ) ITCM 주소가 0x0000 이 아니라 0x1000000에 위치하도록 되어있는 것을 알 수있다.

 위에서 보면 ITCM들은 펌웨어에 의해서 32Mbyte 영역으로 설정되기 때문에 ITCM의 시작 주소를 바꾸는 것은 불가능하지만 mirror를 시켜서 32Mbyte안 쪽에 어디든지 가능하다. 이것을 Protection Unit을 사용해서 ITCM의 위치와 크기를 0x1000000에 있도록 설정해서 ITCM의 위치를 정확히 찝어 주는 것 같다. 상당히 복잡한 구조다. ㅜ_ㅜ

 

3. I/O 포트 맵(Port Map)

 컨트롤러와 연결된 I/O 주소는 http://nocash.emubase.de/gbatek.htm#dsiomaps 에서 찾을 수 있고 아래와 같다.

ARM9 I/O Map
Display Engine A

4000000h  4    2D Engine A - DISPCNT - LCD Control (Read/Write)
  4000004h  2    2D Engine A - DISPSTAT - General LCD Status (Read/Write)
  4000006h  2    2D Engine A - VCOUNT - Vertical Counter (Read only)
  4000008h  50h  2D Engine A (same registers as GBA, some changed bits)
  4000060h  2    DISP3DCNT - ?
  4000064h  4    DISPCAPCNT - Display Capture Control Register (R/W)
  4000068h  4    DISP_MMEM_FIFO - Main Memory Display FIFO (R?/W)
  400006Ch  2    MASTER_BRIGHT - Master Brightness Up/Down
DMA, Timers, and Keypad
40000B0h  30h  DMA Channel 0..3
  40000E0h  10h  DMA FILL Registers for Channel 0..3
  4000100h  10h  Timers 0..3
  4000130h  2    KEYINPUT
  4000132h  2    KEYCNT
IPC/ROM
4000180h  2  IPCSYNC - IPC Synchronize Register (R/W)
  4000184h  2  IPCFIFOCNT - IPC Fifo Control Register (R/W)
  4000188h  4  IPCFIFOSEND - IPC Send Fifo (W)
  40001A0h  2  AUXSPICNT - Gamecard ROM and SPI Control
  40001A2h  2  AUXSPIDATA - Gamecard SPI Bus Data/Strobe
  40001A4h  4  Gamecard bus timing/control
  40001A8h  8  Gamecard bus 8-byte command out
  40001B0h     romcrypt - (not sure if encryption can be accessed by arm9...?)
Memory and IRQ Control
4000204h  2  EXMEMCNT - External Memory Control (R/W)
  4000208h  2  IME - Interrupt Master Enable (R/W)
  4000210h  4  IE  - Interrupt Enable (R/W)
  4000214h  4  IF  - Interrupt Request Flags (R/W)
  4000240h  1  VRAMCNT_A - VRAM-A (128K) Bank Control (W)
  4000241h  1  VRAMCNT_B - VRAM-B (128K) Bank Control (W)
  4000242h  1  VRAMCNT_C - VRAM-C (128K) Bank Control (W)
  4000243h  1  VRAMCNT_D - VRAM-D (128K) Bank Control (W)
  4000244h  1  VRAMCNT_E - VRAM-E (64K) Bank Control (W)
  4000245h  1  VRAMCNT_F - VRAM-F (16K) Bank Control (W)
  4000246h  1  VRAMCNT_G - VRAM-G (16K) Bank Control (W)
  4000247h  1  WRAMCNT   - WRAM Bank Control (W)
  4000248h  1  VRAMCNT_H - VRAM-H (32K) Bank Control (W)
  4000249h  1  VRAMCNT_I - VRAM-I (16K) Bank Control (W)
Maths
4000280h  2  DIVCNT - Division Control (R/W)
  4000290h  8  DIV_NUMER - Division Numerator (R/W)
  4000298h  8  DIV_DENOM - Division Denominator (R/W)
  40002A0h  8  DIV_RESULT - Division Quotient (=Numer/Denom) (R/W?)
  40002A8h  8  DIVREM_RESULT - Division Remainder (=Numer MOD Denom) (R/W?)
  40002B0h  2  SQRTCNT - Square Root Control (R/W)
  40002B4h  4  SQRT_RESULT - Square Root Result (R/W?)
  40002B8h  8  SQRT_PARAM - Square Root Parameter Input (R/W)
  4000300h  4  POSTFLG - Undoc
  4000304h  2  POWCNT1 - Graphics Power Control Register (R/W)
3D Display Engine
4000320h..6A3h
Display Engine B
4001000h  4    2D Engine A - DISPCNT - LCD Control (Read/Write)
  4001008h  50h  2D Engine B (same registers as GBA, some changed bits)
  400106Ch  2    MASTER_BRIGHT - 16bit - Master Brightness Up/Down

4100000h  4    IPCFIFORECV - IPC Receive Fifo (R)
  4100010h  4    Gamecard bus 4-byte data in, for manual or dma read
Hardcoded RAM Addresses for Exception Handling
27FFD9Ch   ..  NDS9 Debug Stacktop / Debug Vector (0=None)
  DTCM+3FF8h 4   NDS9 IRQ Check Bits (hardcoded RAM address)
  DTCM+3FFCh 4   NDS9 IRQ Handler (hardcoded RAM address)
Main Memory Control
27FFFFEh  2    Main Memory Control
Further Memory Control Registers
ARM CP15 System Control Coprocessor

ARM7 I/O Map
4000004h  2   DISPSTAT
  4000006h  2   VCOUNT
  40000B0h  30h DMA Channels 0..3
  4000100h  10h Timers 0..3
  4000120h  4   debug siodata32
  4000128h  4   debug siocnt
  4000130h  2   keyinput
  4000132h  2   keycnt
  4000134h  2   debug rcnt
  4000136h  2   EXTKEYIN
  4000138h  1   RTC Realtime Clock Bus
  4000180h  2   IPCSYNC - IPC Synchronize Register (R/W)
  4000184h  2   IPCFIFOCNT - IPC Fifo Control Register (R/W)
  4000188h  4   IPCFIFOSEND - IPC Send Fifo (W)
  40001A0h  2   AUXSPICNT - Gamecard ROM and SPI Control
  40001A2h  2   AUXSPIDATA - Gamecard SPI Bus Data/Strobe
  40001A4h  4   Gamecard bus timing/control
  40001A8h  8   Gamecard bus 8-byte command out
  40001B0h  4   Gamecard Encryption
  40001B4h  4   Gamecard Encryption
  40001B8h  2   Gamecard Encryption
  40001BAh  2   Gamecard Encryption
  40001C0h  2   SPI bus Control (Firmware, Touchscreen, Powerman)
  40001C2h  2   SPI bus Data
  4000204h  2   EXMEMSTAT - External Memory Status
  4000206h  2   WIFIWAITCNT
  4000208h  4   IME
  4000210h  4   IE
  4000214h  4   IF
  4000240h  1   VRAMSTAT - VRAM-C,D Bank Status (R)
  4000241h  1   WRAMSTAT - WRAM Bank Status (R)
  4000300h  1   POSTFLG
  4000301h  1   HALTCNT (different bits than on GBA) (plus NOP delay)
  4000304h  2   POWCNT2  Sound/Wifi Power Control Register (R/W)
  4000308h  4   BIOSPROT - Bios-data-read-protection address
Sound Registers
4000400h 100h Sound Channel 0..15 (10h bytes each)
  40004x0h  4  SOUNDxCNT - Sound Channel X Control Register (R/W)
  40004x4h  4  SOUNDxSAD - Sound Channel X Data Source Register (W)
  40004x8h  2  SOUNDxTMR - Sound Channel X Timer Register (W)
  40004xAh  2  SOUNDxPNT - Sound Channel X Loopstart Register (W)
  40004xCh  4  SOUNDxLEN - Sound Channel X Length Register (W)
  4000500h  2  SOUNDCNT - Sound Control Register (R/W)
  4000504h  2  SOUNDBIAS - Sound Bias Register (R/W)
  4000508h  1  SNDCAP0CNT - Sound Capture 0 Control Register (R/W)
  4000509h  1  SNDCAP1CNT - Sound Capture 1 Control Register (R/W)
  4000510h  4  SNDCAP0DAD - Sound Capture 0 Destination Address (W?)
  4000514h  2  SNDCAP0LEN - Sound Capture 0 Length (R/W)
  4000518h  4  SNDCAP1DAD - Sound Capture 1 Destination Address (W?)
  400051Ch  2  SNDCAP1LEN - Sound Capture 1 Length (R/W)
gamecart...
4100000h  4   IPCFIFORECV - IPC Receive Fifo (R)
  4100010h  4   Gamecard bus 4-byte data in, for manual or dma read
WLAN Registers
4808036h  2   W
  4808158h  2   W
  480815Ah  2   W
  480815Ch  2   R
  480815Eh  2   R
  4808160h  2   W
  4808168h  2   W
  480817Ch  2   W
  480817Eh  2   W
  4808180h  2   R
  4808184h  2   W
Hardcoded RAM Addresses for Exception Handling
 
380FFDCh  ..  NDS7 Debug Stacktop / Debug Vector (0=None)
  380FFF8h  4   NDS7 IRQ Check Bits (hardcoded RAM address)
  380FFFCh  4   NDS7 IRQ Handler (hardcoded RAM address)

 

 

4. 마치며...

 일단 간단하게 NDS의 메모리 맵에 대해 알아보았다. 시스템 프로그램을 하려면 메모리맵은 필수이므로 자주 참고하도록 하자.

 

 

이 글은 스프링노트에서 작성되었습니다.

01 NDS 롬(ROM) 파일 포맷(Format) 분석

원문 : http://kkamagui.springnote.com/pages/417484

 

들어가기 전에...

0. 시작하면서...

 devkitARM이 *.nds 파일을 만드는 과정을 살펴보면 makefile을 참조해서 살펴보면 아래와 같은 과정을 거친다.

  1. object 파일을 링크하여 *.elf 파일을 만든다.
  2. *.elf 파일을 objcopy -O binary 옵션을 이용하여 코드와 데이터 영역만 추려내서 순수한 바이너리 파일인 *.arm9 or *.arm7 파일을 만든다.
  3. ndstool을 사용해서 *.arm9파일을 *.nds 파일로 만든다.

 

 그럼 도대체 이 NDS Rom 파일은 어떤 구조로 되어있는 것일까? 자세히 살펴보자.

 

1. NDS 롬(ROM) 헤더(Header) 구조

http://www.bottledlight.com/ds/index.php/FileFormats/NDSFormat를 참조하면 실제 어떤 구조로 되어있는지 알 수 있다.

Header format

Field Start End Size Example data (Metroid demo)
Game title 0x000 0x00B 12 "FIRST HUNT "
Game code 0x00C 0x00F 4 "AMFE"
Maker code 0x010 0x011 2 "01" (Nintendo)
Unit code 0x012 0x012 1 0x00
Device code 0x013 0x013 1 0x00
Card size 0x014 0x014 1 0x07 (2^(20 + 7) = 128Mb = 16 MB)
Card info 0x015 0x01E 10 0x00's
Flags 0x01F 0x01F 1 0x00
ARM9 source (ROM) 0x020 0x023 4 0x00004000 (must be 4 KB aligned)
ARM9 execute addr 0x024 0x027 4 0x02004800
ARM9 copy to addr 0x028 0x02B 4 0x02004000
ARM9 binary size 0x02C 0x02F 4 0x00081D58
ARM7 source (ROM) 0x030 0x033 4 0x000B3000
ARM7 execute addr 0x034 0x037 4 0x02380000
ARM7 copy to addr 0x038 0x03B 4 0x02380000
ARM7 binary size 0x03C 0x03F 4 0x00026494
Filename table offset (ROM) 0x040 0x043 4 0x000D9600
Filename table size 0x044 0x047 4 0x11B6
FAT offset (ROM) 0x048 0x04B 4 0x000DA800
FAT size 0x04C 0x04F 4 0x678
ARM9 overlay src (ROM) 0x050 0x053 4 0x00085E00
ARM9 overlay size 0x054 0x057 4 0x60
ARM7 overlay src (ROM) 0x058 0x05B 4 0
ARM7 overlay size 0x05C 0x05F 4 0
Control register flags for read 0x060 0x063 4 0x00586000
Control register flags for init 0x064 0x067 4 0x001808F8
Icon+titles (ROM) 0x068 0x06B 4 0x000DB000
Secure CRC16 0x06C 0x06D 2 0xC44D
ROM timeout 0x06E 0x06F 2 0x051E
ARM9 unk addr 0x070 0x073 4 0x020049EC
ARM7 unk addr 0x074 0x077 4 0x02380110
Magic number for unencrypted mode 0x078 0x07F 8 0x00's
ROM size 0x080 0x083 4 0x00EE3E44
Header size 0x084 0x087 4 0x4000
Unknown 5 0x088 0x0BF 56 0x00's
GBA logo 0x0C0 0x15B 156 data
Logo CRC16 0x15C 0x15D 2 0xCF56
Header CRC16 0x15E 0x15F 2 0x00F8
Reserved 0x160 0x1FF 160 0x00's

 

The control register flags contain latency settings and depends on the ROM manufacturer (Macronix, Matrix Memory).

Unknown2a and 'Header Size' contain flags that are (somewhat) used during boot as part of card CR writes. Unknown2b contains the size of a certain type of transfer done during boot, but it's range checked and cannot be reduced. Unknown3c is seemingly unused, and some code paths get data from 0x160 onward (only 0x170 bytes of a header fetch are actually retained by the BIOS)

Secure CRC16 calculation is performed on the ROM from 0x4000 to 0x7FFF, with an initial value of 0xFFFF.

Logo CRC16 calculation is performed on the header from 0x0C0 to 0x15B, with an initial value of 0xFFFF.

Header CRC16 calculation is performed on the header (after the previous two CRCs are filled) from 0x000 to 0x15D with an initial value of 0xFFFF.

Device code needs lower 3 bits to be 0. This is a similar behavior as GBA header.

 위의 정보를 보면 롬 영역 안쪽에 FAT File System이 들어가는 것을 알 수 있다. 즉 게임에서 필요한 데이터를 내부에 플래쉬에 저장하여 읽고 쓴다는 이야기다. @0@)/~ 참으로 놀라운 사실이 아닐 수 없다.

 저 부분을 분석하면 게임 내부에서 사용하는 데이터도 추출해 낼 수 있다는 말이된다. 물론 좀더 분석해봐야 알겠지만 상당히 흥미로운 사실이다.

  

1.1 ARM9 ROM의 시작 위치

 위에서 Header Size를 보면 0x4000으로 설정되어있다. ARM9 ROM의 시작위치는? 0x4000 인걸 알 수 있다. 옆에 "4Kbyte로 반드시 정렬해라"라는 이야기가 있는데, 게임롬에만 해당하는 건지 확실히 알 수 없지만 굳이 하지 않아도 될 것 같다. 특히 홈브루 같은 경우는 더욱...

 위 결과로 미루어 보면 ROM Header의 바로 뒤에 ARM9의 코드가 들어감을 알 수 있다.

  

1.2 ARM7 ROM의 시작 위치

 ARM7의 코드가 포함된 ROM의 위치는 ARM9의 바로 뒤에 연결 되어있지 않다. 다만 0x1000의 배수(4Kbyte)에 맞추어서 시작하고 있다.

 ARM7 코드가 로딩되는 위치의 주소를 보면 0x2380000으로 4Mybte의 메인 메모리 뒷쪽에 자리잡고 있다. NDS 게임의 경우 ARM7이 하는 일이 그렇게 많지 않아 뒷쪽으로 할당한 것 처럼 보인다.

  

1.3 롬 실행 시 메모리 배치

 롬 헤더에 보면 롬파일 내에 ARM9/7 코드가 어디서 시작하고 크기는 얼마나 되며 메모리에 어느 위치에 복사해주는지 정보가 나와있다. 또한 Entry 정보도 나와있는 걸로 보아 시작 시에 해당 메모리 위치로 코드를 복사하고 시작함을 알 수 있다.

  

2.아이콘(Icon) 및 광고(Banner) 헤더(Header) 구조

Icon + logo format (the banner)

This is a structure of size 32+512+32+256*6 = 2112 bytes with the following format:

 

Banner structure

Offset Size Description
0 2 Version (always 1)
2 2 CRC-16 of structure, not including first 32 bytes
4 28 Reserved
32 512 Tile Data
544 32 Palette
576 256 Japanese Title
832 256 English Title
1088 256 French Title
1344 256 German Title
1600 256 Italian Title
1856 256 Spanish Title

 

The icon is a 32x32 picture formed out of 4x4 16 color tiles and a single 16 color palette.

Following the icon data (icon offset + 576 bytes), are 6 unicode game titles, displayed in the DS menu depending on the selected language mode. Each title is padded to 128 characters (256 bytes).

The strings are in the same order as the language choice in Firmware.

 게임 아이콘 및 설명을 넣는 부분 같다. 아이콘이나 게임 타이틀 부분도 ndstool.exe를 이용하면 쉽게 넣을 수 있으므로 나중에 참고하자.

 

3.실제 롬 파일 분석

3.1 헤더 분석

 devkitPro를 설치하면 자동으로 설치되는 ndstool.exe 프로그램을 이용해서 KKAMAGUI NOTEPAD 롬파일을 분석해 보자. KKAMAGUI NOTEPAD의 롬파일은 00 KKAMAGUI NOTEPAD 에서 구할 수 있다. dstool.exe 프로그램은 아래와 같은 다양한 옵션을 가지고 있다.

 

 위의 옵션 중에 -i 옵션을 이용하면 롬의 정보를 볼 수 있다. i 옵션을 이용하여 롬 파일을 분석해 보자.

  1. D:\devkitPro\MyProject\NotePad>ndstool -i NotePad.nds

     

    Nintendo DS rom tool 1.33 - Jan 28 2007 21:02:20
    by Rafael Vuijk, Dave Murphy,  Alexei Karpenko
    Header information:
    0x00    Game title                      .
    0x0C    Game code                       ####
    0x10    Maker code
    0x12    Unit code                       0x00
    0x13    Device type                     0x00
    0x14    Device capacity                 0x01 (2 Mbit)
    0x15    reserved 1                      000000000000000000
    0x1E    ROM version                     0x00
    0x1F    reserved 2                      0x04
    0x20    ARM9 ROM offset                 0x200
    0x24    ARM9 entry address              0x2000000
    0x28    ARM9 RAM address                0x2000000
    0x2C    ARM9 code size                  0x31794
    0x30    ARM7 ROM offset                 0x31A00
    0x34    ARM7 entry address              0x37F8000

    0x38    ARM7 RAM address                0x37F8000
    0x3C    ARM7 code size                  0x12B8
    0x40    File name table offset          0x32E00
    0x44    File name table size            0x9
    0x48    FAT offset                      0x33000
    0x4C    FAT size                        0x0
    0x50    ARM9 overlay offset             0x0
    0x54    ARM9 overlay size               0x0
    0x58    ARM7 overlay offset             0x0
    0x5C    ARM7 overlay size               0x0
    0x60    ROM control info 1              0x007F7FFF
    0x64    ROM control info 2              0x203F1FFF
    0x68    Icon/title offset               0x0
    0x6C    Secure area CRC                 0x0000 (-, homebrew)
    0x6E    ROM control info 3              0x051E
    0x70    ARM9 ?                          0x0
    0x74    ARM7 ?                          0x0
    0x78    Magic 1                         0x00000000
    0x7C    Magic 2                         0x00000000
    0x80    Application end offset          0x00033000
    0x84    ROM header size                 0x00000200
    0xA0    ?                               0x4D415253
    0xA4    ?                               0x3131565F
    0xA8    ?                               0x00000030
    0xAC    ?                               0x53534150
    0xB0    ?                               0x00963130
    0x15C   Logo CRC                        0x9E1A (OK)
    0x15E   Header CRC                      0x32B1 (OK)

 홈브루의 경우는 게임의 경우와 조금 달리 ARM7의 이미지가 0x1000에서 시작하지 않고 있음을 알 수 있다. 그리고 ARM7의 코드가 4Mbyte의 공유영역에 올라가있는 것이 아니라 0x37F8000 주소에 Private RAM(64Kbyte)에 위치하고 있다. 02 NDS Spec에 보면 메모리 맵에 대한 자세한 내용을 알 수 있다.

 Secure area CRC 부분을 보면 이 값이 0x0000 이고 homebrew라고 표시되어있다. 실제 게임은 이 값이 0이 아닌 것일까? 몇가지 게임 롬을 분석해본 결과... 0x0000 이 아님을 알 수 있었다. 보안에 관련된 영역을 검사하는데 사용되는 것으로 생각된다.

 

3.2 FAT 영역 분석

 롬 파일 내부에 FAT 영역을 가진다. ndstool.exe 를 이용하면 -l 옵션으로 FAT 영역의 데이터를 볼 수 있다. KKAMAGUI NOTEPAD의 경우 FAT 영역에 별다른 데이터를 가지지 않으므로 다른 홈브루나 게임 롬을 열어봐야 된다. 홈브루 중에서는 아마 찾기 힘들 것이므로 게임 롬을 열어보자. 디렉토리 구조와 파일들이 줄줄이 나오는 것을 알 수 있다.

FATList.PNG

<ndstool.exe를 이용해서 롬 파일의 FAT 영역을 분석한 화면>

 

 그렇다면 ROM 파일에 있는 FAT 파일 시스템은 우리가 윈도우에서 포맷할 때 만들어지는 모든 파일 시스템 정보를 포함하고 있을까?

 이 부분은 ROM 파일을 열어서 확인을 해봐야 할 것 같다.

 

여기 체우기...

 

4. 데이터 영역 분석

 롬 데이터 영역은 .arm9 및 .arm7의 파일의 덤프 형태로 되어잇는데, 자세한 내용은 00 NDS makefile 및 NDS 파일 생성 과정 분석 페이지를 참조하자.

 

마치며...

 이상으로 NDS 롬 파일 구조에 대해서 알아보았다. 특별히 암호화도 되어있지 않고 checksum을 이용해서 롬 파일의 유효성을 체크한다.

 롬 파일의 구조를 알았으니, 그 안에서 데이터를 뽑아내어 언어를 한글화 한다던지, 그림 파일을 바꾼다던지 하는 것고 그리 어렵지 않을꺼라 생각한다.

 다시 NDS의 세계로 빠져보자 @0@)/~~

 이 글은 스프링노트에서 작성되었습니다.


01 libfat 업그레이드

원문 : http://kkamagui.springnote.com/pages/407195

 

들어가기 전에...

 

0. 시작하면서...

 홈 브루를 개발하다보면 자료를 저장하거나 데이터를 읽어올 필요가 있다. 이를 위히 FAT로 포맷된 카드에서 자료를 읽을 수 있도록 libfat라는 라이브러리를 지원한다. 하지만 최신의 장비와 같은 경우, Devkit pro에 포함된 기본 libfat.a 라이브러리에서 지원하지 않을 가능성이 크다.

 내가 가진 장비를 지원하기위해 libfat 라이브러리를 업그레이드 하는 과정을 살펴보자.

 

1. libfat 소스 다운로드 및 링크

 개발한 홈 브루가 데이터를 읽고 쓰려면 FAT 라이브러리가 있어야 한다. 그리고 당연히 하드웨어적인 섹터 IO를 수행하는 라이브러리가 있어야 한다. 기존의 Devkit Pro에 포함된 libfat.a가 IO를 지원하지 않는다는 가정하에 http://sourceforge.net/project/downloading.php?group_id=114505&use_mirror=nchc&filename=libfat-src-20070127.tar.bz2&12016483 에서 libfat 소스를 다운받아서 추가한 후 컴파일 하는 과정을 살펴보자.

 일단 현재( 2007/08/08 21:04:06 )까지 최신 버전인 libfat-src-20070127.tar.tar 파일을 다운 받아서 devkit pro를 설치한 폴더에 압축을 푼다. 그럼 아래와 같은 폴더 구조를 가질 것이다.

libfat1.PNG

 

  여기서 disk_io 폴더에 들어가서 disk.c 파일을 열고 아래와 같은 부분을 찾아본다.

  1. const IO_INTERFACE* ioInterfaces[] = {
     &_io_dldi,  // Reserved for new interfaces
    #ifdef NDS
     // Place Slot 1 (DS Card) interfaces here
     &_io_njsd, &_io_nmmc,
    #endif
     // Place Slot 2 (GBA Cart) interfaces here
     &_io_mpcf, &_io_m3cf, &_io_sccf, &_io_scsd, &_io_m3sd
    };

  위의 코드에서 보듯이 DLDI와 몇몇 장비만 추가적으로 지원한다는 것을 알 수 있다. 고로 내 장비의 이름이 위에 없다면 해당 장비를 판매하는 곳에 가서 libfat 용 파일을 Low IO를 수행하는 파일을 받아야 한다. 각 장비 개발사에 가면 아래와 같은 파일들을 구할 수 있을 것이다.

libfat2.PNG

 이제 이 파일을 disk_io 폴더에 복사해 넣고 io_r4tf.h 파일을 아래와 같이 수정한다.

  1. #ifndef IO_R4TF_H
    #define IO_R4TF_H
  2. // 'R4TF'
    #define DEVICE_TYPE_R4TF 0x46543452
  3. // 추가한 부분
    #ifdef NDS
    #define SUPPORT_R4TF
    #endif
  4. #include "disc_io.h"
  5. // 추가한 부분
    typedef IO_INTERFACE* LPIO_INTERFACE;
  6. // 수정한 부분
    extern IO_INTERFACE* R4TF_GetInterface(void);
  7. // 추가한 부분
    extern IO_INTERFACE io_r4tf;
  8. #endif

 

 그리고 disk.c 파일을 열어서 위에서 언급했던 부분을 아래와 같이 수정한다.

  1. const IO_INTERFACE* ioInterfaces[] = {
     &_io_dldi,  // Reserved for new interfaces
    #ifdef NDS
     // Place Slot 1 (DS Card) interfaces here
     &io_r4tf, //&_io_njsd, &_io_nmmc,
    #endif
     // Place Slot 2 (GBA Cart) interfaces here
     &_io_mpcf, &_io_m3cf, &_io_sccf, &_io_scsd, &_io_m3sd
    };

 

 자 이제 수정이 다 끝났으니 build를 수행할 차례이다. cmd.exe 를 실행시켜서 libfat가 있는 소스로 이동한다음 make를 입력하면 아래와 같이 컴파일 및 링크가 시작된다.

libfat3.PNG

<build 진행중>

 libfat4.PNG

<build 완료>

 

 위처럼 빌드 완료 화면이 나오지 않고 에러가 표시되면 잘못 수정한 것이므로 에러 메시지를 보고 마저 수정한 후 처리하도록 한다. 빌드가 완료되고 나면 libfat 폴더 밑의 nds 폴더 밑의 lib 폴더에 libfat.a 파일이 생긴걸 알 수 있다.

libfat5.PNG

<libfat.a 파일 정상 생성>

 

 위에서 생성된 파일을 아래와 같이 libnds 폴더 아래에 lib 폴더에 복사하면 설치가 끝난다.

libfat6.PNG

 

 

2. 테스트

 libfat 라이브러리를 빌드했으니 테스트를 한번 해보자. 아래는 directory를 탐색하고 파일을 생성해서 데이터를 쓰는 소스이다.

  1. #include <nds.h>
    #include <fat.h>
    #include <stdio.h>
    #include <sys/dir.h>
  2. bool TestFile( void );
  3. int main(void)
    {
        // LCD Core를 켠다.
        powerON( POWER_ALL_2D );
  4.     // 파일을 테스트 한다.
        TestFile();
  5.     while(1)
        {
            // 아무 interrupt나 대기한다.
            swiWaitForIRQ();
        }
       
        return 0;
    }
  6. /**
        파일을 테스트 한다.
    */
    bool TestFile( void )
    {
        char vcBuffer[ 256 ];
        FILE* fp;
        struct stat st;
        char filename[256];
        int i;
  7.     if( ( fatInitDefault() == false ) )
        {
            return false;
        }
  8.     // Directory Test
        DIR_ITER* dir;
        dir = diropen("/");
        if( dir == NULL )
        { 
            //iprintf("Unable to open the directory.\n");
        }
        else
        { 
            i = 0;
            while( dirnext( dir, filename, &st ) == 0 )
            {  
                // st.st_mode & S_IFDIR indicates a directory  
                //sprintf( vcBuffer, "%s: %s",
                //    (st.st_mode & S_IFDIR ? " DIR" : "FILE"), filename);
            }
        }
  9.     dirclose( dir );
  10.         
        // File Test
        //fp = fopen("fat1:/a.txt", "wb+");
        fp = fopen("/a.txt", "wb+");
        if( fp == NULL )
        {
            return false;
        }
  11.     // 읽기 테스트
        if( fread( vcBuffer, 1, sizeof( vcBuffer), fp ) != 0 )
        {
            // TODO Something...
        }
           
        if( fwrite( "testfile", 1, 8, fp ) <= 0 )
        {
            fclose( fp );
            return false;
        }
       
        fclose( fp );
        return true;
    }

 위에서 보는 것과 같이 파일 관련 함수인 fopen/fread/fwrite/fclose 함수와 디렉토리 관련 함수인 diropen/dirnext/dirclose를 그대로 사용할 수 있음을 알 수 있다.

 실행 결과 나온 프로그램을 NDS에서 실행하면 그냥 하얀 화면만 뜰 것이다. 그대로 종료한 후 다시 디스크를 열어보면 a.txt 파일이 생기고 그 안에 "testfile"이라는 내용이 들어있음을 확인 할 수 있다. 자세한 소스는 첨부 파일로 추가했다.

 만약 실행했는데, 정상적으로 파일이 생성되지 않았다면 라이브러리가 정상적으로 생성되지 않았기 때문이므로 위의 순서대로 다시 빌드해서 사용하도록 하자.

 

3. 첨부

 

 

 

이 글은 스프링노트에서 작성되었습니다.

00 NDS 홈브루 프로젝트 생성

원문 :  http://kkamagui.springnote.com/pages/415865

 

들어가기 전에...

 

1.필수 파일

 NDS 홈브루 프로젝트를 만들고 NDS 롬 파일을 생성하기위해서는 최소 2개의 파일과 1개의 폴더가 필요하다.

 필요한 파일은 Makefile과 Main.cpp 이고 필요한 폴더는 Source 폴더이다. 폴더 이름을 Source 말고 다른 것으로 하면 컴파일이 안된다. 그 이유는 makefile에 포함된 아래와 같은 내용 때문이다.

  1. ......
  2. TARGET  := $(shell basename $(CURDIR))
    BUILD  := build
    SOURCES  := gfx source data 
    INCLUDES := include build
  3. ......
  4. CFILES  := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
    CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
    SFILES  := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
    BINFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.bin)))
  5. ......

 위에서 보는 것과 같이 gfx, source, data 폴더 아래에 있는 전체 폴더를 돌면서 폴더가 아닌 *.c *.cpp *.s *.bin 파일을 찾도록 되어있기 때문이다. makefile에 대한 자세한 내용은 00 NDS makefile 분석 파일을 참조하도록 하자.

 

 만약 NDS 홈브루 개발을 위한 최소한의 파일과 폴더만을 이용해서 구성한다면 아래와 같을 것이다.

최소프로젝트1.PNG    최소프로젝트2.PNG

<홈브루 생성을 위한 최소한의 파일 및 폴더>

 

2.main.cpp 및 기타 파일 구성

  main.cpp에 있는 내용은 examples 폴더에있는 Helloworld 예제로 복사해넣었다. 만약 다수의 프로젝트 파일이 있다면 위에서 언급한 폴더에 넣거나 그 하위 폴더를 생성해서 넣으면 된다.

  1. /*---------------------------------------------------------------------------------
  2.  $Id: main.cpp,v 1.7 2006/06/18 21:32:41 wntrmute Exp $
  3.  Simple console print demo
     -- dovoto
  4.  $Log: main.cpp,v $
     Revision 1.7  2006/06/18 21:32:41  wntrmute
     tidy up examples
     
     Revision 1.6  2005/09/16 12:20:32  wntrmute
     corrected iprintfs
     
     Revision 1.5  2005/09/12 18:32:38  wntrmute
     removed *printAt replaced with ansi escape sequences
     
     Revision 1.4  2005/09/05 00:32:19  wntrmute
     removed references to IPC struct
     replaced with API functions
     
     Revision 1.3  2005/08/31 03:02:39  wntrmute
     updated for new stdio support
     
     Revision 1.2  2005/08/03 06:36:30  wntrmute
     added logging
     added display of pixel co-ords
  5. ---------------------------------------------------------------------------------*/
    #include <nds.h>
  6. #include <stdio.h>
  7. volatile int frame = 0;
  8. //---------------------------------------------------------------------------------
    void Vblank() {
    //---------------------------------------------------------------------------------
     frame++;
    }
     
    //---------------------------------------------------------------------------------
    int main(void) {
    //---------------------------------------------------------------------------------
     touchPosition touchXY;
  9.  irqInit();
     irqSet(IRQ_VBLANK, Vblank);
     irqEnable(IRQ_VBLANK);
     videoSetMode(0); //not using the main screen
     videoSetModeSub(MODE_0_2D | DISPLAY_BG0_ACTIVE); //sub bg 0 will be used to print text
     vramSetBankC(VRAM_C_SUB_BG);
  10.  SUB_BG0_CR = BG_MAP_BASE(31);
     
     BG_PALETTE_SUB[255] = RGB15(31,31,31); //by default font will be rendered with color 255
     
     //consoleInit() is a lot more flexible but this gets you up and running quick
     consoleInitDefault((u16*)SCREEN_BASE_BLOCK_SUB(31), (u16*)CHAR_BASE_BLOCK_SUB(0), 16);
  11.  iprintf("      Hello DS dev'rs\n");
     iprintf("     www.devkitpro.org\n");
     iprintf("   www.drunkencoders.com");
  12.  while(1) {
     
      swiWaitForVBlank();
      touchXY=touchReadXY();
  13.   // print at using ansi escape sequence \x1b[line;columnH
      iprintf("\x1b[10;0HFrame = %d",frame);
      iprintf("\x1b[16;0HTouch x = %04X, %04X\n", touchXY.x, touchXY.px);
      iprintf("Touch y = %04X, %04X\n", touchXY.y, touchXY.py);  
     
     }
  14.  return 0;
    }

 

 

3.실행

 00 NDS 개발 킷(Devkit Pro) 설치 에 나와있는 컴파일 및 링크 방법을 이용해서 NDS 롬파일을 만들고 iDeaS에서 실행한 결과이다.

최소프로젝트3.PNG

 

4.마치며...

 makefile이 상당히 편리하게 되어있는 관계로 그리 어렵지않게 새 프로젝트를 생성할 수 있었다. 이제 홈브루의 세계로 빠져보자. @0@)/~

 

이 글은 스프링노트에서 작성되었습니다.

00 NDS 개발 킷(Devkit Pro) 설치

원문 : http://kkamagui.springnote.com/pages/407093

 

들어가기 전에...

 

0. 시작하면서...

 프로그램 개발을 위해서 개발 툴 킷의 설치는 필수이다. 개발 툴 킷이 얼마나 잘 만들어져 있느냐에 따라서 프로그램의 질까지 바뀌어 질 수 있다. NDS 또는 NDSL 개발을 위해 개발 툴 킷의 설치가 필수인데, Devkit Pro라는 툴 킷이 대표적이고 거의 유일하다.

 Devkit Pro는 http://www.devkitpro.org 에 가면 받을 수 있으며 NDS은 ARM9과 ARM7을 가지고 있으므로 DevkitARM을 받아야 한다. Devkit Pro는 PSP용 개발 툴인 Devkit PSP도 가지고 있으니 관심이 있으면 참고하자.

 

1.업데이트 파일 다운로드


http://sourceforge.net/project/showfiles.php?group_id=114505로 이동하면 Devkit Pro의 ARM Download 사이트로 이동하면데 현재( 2007/08/08 19:50:45 )까지 최신 릴리즈는 Release 20(2007/01/29) 버전이다. 다운로드하기 위해 클릭하면 소스 포지(Sourceforge)로 이동하는데, 여기서 자동 업데이트 설치 파일을 다운로드 하자.

 Devkit1.PNG

<다운로드 할 파일>

 만약 설치파일을 정상적으로 다운로드 할 수 없다면 02 NDS 에서 다운 받도록 하자.

 

2.업데이트 실행

 다운을 받고 나면 설치 과정을 거쳐야 한다. 다운로드한 DevkitPro Updater 파일을 적당한 폴더를 생성해서 넣고 실행하자. 그럼 아래와 같은 화면이 표시된다.

Devkit2.PNG

<업데이트 화면>

 위의 화면이 표시되면 Next 버튼을 눌러 파일을 다운 받자. 아래는 다운로드가 진행중이고 완료된 후 완전히 설치가 끝난 화면이다.

Devkit3.PNG

<다운로드 진행중>

Devkit4.PNG

<설치 완료>

 만약 설치파일을 정상적으로 다운로드 할 수 없다면 02 NDS 에서 다운 받도록 하자. d:\ndsl_develop 폴더에 설치를 했으니 해당 폴더로 이동하면 설치된 결과를 확인할 수 있다.

 

3.에뮬레이터(Emulator 설치)

 개발된 홈브루를 NDS에 직접 옮겨서 테스트하는 방법도 나쁘진 않지만, 파일을 옮기고 NDS를 재부팅하는 과정이 불편하다. NDS 에뮬레이터 프로그램을 이용하면 이러한 과정을 줄일 수 있으며 편리하게 개발할 수 있다.

 에뮬레이터 프로그램은 몇가지가 있는데, 그중 2가지 정도만 설치하면 테스트하는데 큰 문제는 없다.

 위의 2가지를 받아서 압축을 풀면 된다. 홈브루를 테스트하기위해서는 반복해서 실행해야 하므로 실행하기 편리한 곳에 압축을 풀자.

 

iDeas.PNG     nogba1.PNG

<iDeaS(좌측)과 No$gba(우측) 실행화면>

 

4.테스트 프로그램 컴파일 및 링크

  개발 툴킷이 정상적으로 설치되었다면 폴더의 내용은 아래와 비슷할 것이다.

nds폴더.PNG

<NDS 개발 폴더>

 그럼 이제 예제 프로그램을 하나 실행해 보자. examples 폴더의 하위에 보면 gba/gp32/nds 별 예제 프로그램들이 있다.

 

4.1 Programmer's Notepad2 사용

 이것을 빌드한 후에 3D 그래픽 예제를 한번 실행해 보자. nds->Graphics->Display_List_2로 이동하면 Display_List_2.pnproj 파일이 보일 것이다. 이것을 더블 클릭하면 아래와 같이 Programmers Notepad 2 프로그램이 뜬다.

nds_notepad2.PNG

 

<Programmers Notepad- Make 실행화면> 

 Tools 메뉴에서 make를 선택하거나 단축키인 Alt+1을 누르면 make를 실행할 수 있다. 위의 화면에서 아래쪽에 output을 보면 make가 정상적으로 실행된 것을 알 수 있는데, 다시 폴더로 가보면 Display_List_2.nds/.elf/.arm9 파일이 생긴 것을 확인할 수 있다.

 이것을 no$gba에 넣고 실행하면 아래와 같은 많이 보던 화면이 뜬다.

nogba1.PNG nogba2.PNG

<Display_List_2.nds 실행화면>

 바로 DirectX의 기본 프로젝트다. @0@)/~~ 이것을 iDeas에서도 돌릴 수 있지만 3D 가속이 느려서 굉장히 끊어진다. 예제가 많으니 하나하나 실행해서 돌려보자.

 각 에뮬레이터마다 특색이 있어서 둘다 실행해 봐야 할 것이다.

 

4.2 콘솔(console) 또는 다른 IDE 사용

 콘솔(cmd.exe)을 띄워서 해당 소스 폴더로 이동한 후, make를 입력하거나 IDE의 명령실행 창에서 make를 입력하면 컴파일 및 링크를 실행할 수 있다.

 make.PNG

 

 

5.마치면서...

 지금까지 NDS 개발 툴 킷인 Devkit Pro를 설치하고 예제 프로그램을 컴파일/링크하여 실행하는 과정을 알아보았다. NDS가 없어도 에뮬레이터가 있기 때문에 마음만 먹으면 누구나 할 수 있다. 우리 모두 NDS 홈브루의 세계로 빠져보자. @ㅁ@)/~~

 

6.첨부

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

00 NDS makefile 및 NDS 파일 생성 과정 분석

원본 :  http://kkamagui.springnote.com/pages/415876

 

들어가기 전에...

 

0,시작하면서...

 새로운 프로젝트를 생성하고 소스파일을 생성한 후 컴파일 및 링크를 거치면 *.nds 파일이 생기게 된다. C/CPP 파일을 이용해서 어떻게 NDS 롬 파일 형태를 만드는 걸까?

 소스 파일에서 NDS 파일이 만들어지기까지 아래의 단계를 거치게 된다.

  1. c/cpp 파일을 컴파일 하여 .o 파일 생성
  2. .o 파일을 링크하여 .elf 파일 생성
  3. .elf 파일을 objcopy 프로그램으로 binary 포맷으로 변경, .arm9/.arm7 파일 생성
  4. .arm9/.arm7 파일을 ndstool.exe 프로그램으로 함께 묶어 .nds 파일 생성

  위의 과정을 확실히 알아보기 위해서는 소스가 컴파일 및 링크되는 규칙이 담긴 makefile을 분석해야 한다.

 

1.makefile 분석

 makefile은 아래와 같은 내용으로 되어있다

  1. #---------------------------------------------------------------------------------
    .SUFFIXES:
    #---------------------------------------------------------------------------------
  2. ifeq ($(strip $(DEVKITARM)),)
    $(error "Please set DEVKITARM in your environment. export DEVKITARM=<path to>devkitARM)
    endif
  3. include $(DEVKITARM)/ds_rules
  4. #---------------------------------------------------------------------------------
    # TARGET is the name of the output
    # BUILD is the directory where object files & intermediate files will be placed
    # SOURCES is a list of directories containing source code
    # INCLUDES is a list of directories containing extra header files
    #---------------------------------------------------------------------------------
    TARGET  := $(shell basename $(CURDIR))
    BUILD  := build
    SOURCES  := gfx source data 
    INCLUDES := include build
  5. #---------------------------------------------------------------------------------
    # options for code generation
    #---------------------------------------------------------------------------------
    ARCH := -mthumb -mthumb-interwork
  6. # note: arm9tdmi isn't the correct CPU arch, but anything newer and LD
    # *insists* it has a FPU or VFP, and it won't take no for an answer!
    CFLAGS := -g -Wall -O2\
        -mcpu=arm9tdmi -mtune=arm9tdmi -fomit-frame-pointer\
       -ffast-math \
       $(ARCH)
  7. CFLAGS += $(INCLUDE) -DARM9
    CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions
  8. ASFLAGS := -g $(ARCH)
    LDFLAGS = -specs=ds_arm9.specs -g $(ARCH) -mno-fpu -Wl,-Map,$(notdir $*.map)
  9. #---------------------------------------------------------------------------------
    # any extra libraries we wish to link with the project
    #---------------------------------------------------------------------------------
    LIBS := -lfat -lnds9   <== -lfat는 libfat를 사용하기 위함이다.
     
     
    #---------------------------------------------------------------------------------
    # list of directories containing libraries, this must be the top level containing
    # include and lib
    #---------------------------------------------------------------------------------
    LIBDIRS := $(LIBNDS)
     
    #---------------------------------------------------------------------------------
    # no real need to edit anything past this point unless you need to add additional
    # rules for different file extensions
    #---------------------------------------------------------------------------------
    ifneq ($(BUILD),$(notdir $(CURDIR)))
    #---------------------------------------------------------------------------------
     
    export OUTPUT := $(CURDIR)/$(TARGET)
     
    export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir))
    export DEPSDIR := $(CURDIR)/$(BUILD)
  10. CFILES  := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
    CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
    SFILES  := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
    BINFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.bin)))
     
    #---------------------------------------------------------------------------------
    # use CXX for linking C++ projects, CC for standard C
    #---------------------------------------------------------------------------------
    ifeq ($(strip $(CPPFILES)),)
    #---------------------------------------------------------------------------------
     export LD := $(CC)
    #---------------------------------------------------------------------------------
    else
    #---------------------------------------------------------------------------------
     export LD := $(CXX)
    #---------------------------------------------------------------------------------
    endif
    #---------------------------------------------------------------------------------
  11. export OFILES := $(BINFILES:.bin=.o) \
         $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
     
    export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
         $(foreach dir,$(LIBDIRS),-I$(dir)/include) \
         $(foreach dir,$(LIBDIRS),-I$(dir)/include) \
         -I$(CURDIR)/$(BUILD)
     
    export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
     
    .PHONY: $(BUILD) clean
     
    #---------------------------------------------------------------------------------
    $(BUILD):
     @[ -d $@ ] || mkdir -p $@  <== 확실한 뜻은 모르겠으나 $(BUILD) 디렉토리가 생성되어있거나 디렉토리 만들어라 인것 같음
     @make --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
     
    #---------------------------------------------------------------------------------
    clean:
     @echo clean ...
     @rm -fr $(BUILD) $(TARGET).elf $(TARGET).nds $(TARGET).arm9 $(TARGET).ds.gba
     
     
    #---------------------------------------------------------------------------------
    else
     
    DEPENDS := $(OFILES:.o=.d)
     
    #---------------------------------------------------------------------------------
    # main targets
    #---------------------------------------------------------------------------------
    $(OUTPUT).nds :  $(OUTPUT).arm9
    $(OUTPUT).arm9 : $(OUTPUT).elf
    $(OUTPUT).elf : $(OFILES)
     
    #---------------------------------------------------------------------------------
    %.o : %.bin
    #---------------------------------------------------------------------------------
     @echo $(notdir $<)
     $(bin2o)
     
     
    -include $(DEPENDS)
     
    #---------------------------------------------------------------------------------------
    endif
    #---------------------------------------------------------------------------------------

 make에서 사용되는 괴 문자들이 많이 보이는데, 괴문자에 대한 자세한 내용은 02 간단한 Make 사용법에서 보도록하고 $@는 ':' 의 좌측을 의미하고 $<는 우측을 의미한다는 것 정도만 알아두자.

 재미있는 점은 코드가 thumb 모드로 생성된다는 것이다. thumb 모드는 32bit 명령이 16bit로 줄어든 형태로써 코드의 크기를 30% 정도 더 줄일 수 있는 장점이 있다.

 

 include하여 사용하고 있는 ds_rules 파일을 살펴보자. ds_rules 파일은 devkitARM 폴더 아래에 있고 아래와 같은 내용을 담고 있다.

  1. ifeq ($(strip $(DEVKITPRO)),)
    $(error "Please set DEVKITPRO in your environment. export DEVKITPRO=<path to>devkitPro)
    endif
  2. include $(DEVKITARM)/base_rules
  3. LIBNDS := $(DEVKITPRO)/libnds
  4. #---------------------------------------------------------------------------------
    %.ds.gba: %.nds
     @dsbuild $<
     @echo built ... $(notdir $@)
  5. #---------------------------------------------------------------------------------
    %.nds: %.arm9
     @ndstool -c $@ -9 $<
     @echo built ... $(notdir $@)
  6. #---------------------------------------------------------------------------------
    %.arm9: %.elf
     @$(OBJCOPY) -O binary $< $@
     @echo built ... $(notdir $@)
     
    #---------------------------------------------------------------------------------
    %.arm7: %.elf
     @$(OBJCOPY) -O binary $< $@
     @echo built ... $(notdir $@)
  7. #---------------------------------------------------------------------------------
    %.elf:
     @echo linking $(notdir $@)
     @$(LD)  $(LDFLAGS) $(OFILES) $(LIBPATHS) $(LIBS) -o $@

 어떻게 하여 NDS 파일을 만드는지 위를 보면 확실히 알 수 있다 rule에 의해서 elf -> arm9 -> nds -> ds.gba 파일이 만들어지는 것이다. 그럼 ds_rules에서 inlcude하는 base_rules는 어떻게 구성되어있을까?

 

 아래는 base_rules 내용이다.

  1. #---------------------------------------------------------------------------------
    # path to tools - this can be deleted if you set the path in windows
    #---------------------------------------------------------------------------------
    export PATH  := $(DEVKITARM)/bin:$(PATH)
  2. #---------------------------------------------------------------------------------
    # the prefix on the compiler executables
    #---------------------------------------------------------------------------------
    PREFIX  := arm-eabi-
  3. export CC := $(PREFIX)gcc
    export CXX := $(PREFIX)g++
    export AS := $(PREFIX)as
    export AR := $(PREFIX)ar
    export OBJCOPY := $(PREFIX)objcopy
  4. #---------------------------------------------------------------------------------
    %.a:
    #---------------------------------------------------------------------------------
     @echo $(notdir $@)
     @rm -f $@
     $(AR) -rc $@ $^
  5. #---------------------------------------------------------------------------------
    %.arm.o: %.arm.cpp
     @echo $(notdir $<)
     $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) -marm-c $< -o $@
     
    #---------------------------------------------------------------------------------
    %.arm.o: %.arm.c
     @echo $(notdir $<)
     $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) -marm -c $< -o $@
  6. #---------------------------------------------------------------------------------
    %.iwram.o: %.iwram.cpp
     @echo $(notdir $<)
     $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) -marm -mlong-calls -c $< -o $@
     
    #---------------------------------------------------------------------------------
    %.iwram.o: %.iwram.c
     @echo $(notdir $<)
     $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) -marm -mlong-calls -c $< -o $@
  7. #---------------------------------------------------------------------------------
    %.itcm.o: %.itcm.cpp
     @echo $(notdir $<)
     $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) -marm -mlong-calls -c $< -o $@
     
    #---------------------------------------------------------------------------------
    %.itcm.o: %.itcm.c
     @echo $(notdir $<)
     $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) -marm -mlong-calls -c $< -o $@
  8. #---------------------------------------------------------------------------------
    %.o: %.cpp
     @echo $(notdir $<)
     $(CXX) -MMD -MP -MF $(DEPSDIR)/$*.d $(CXXFLAGS) -c $< -o $@
     
    #---------------------------------------------------------------------------------
    %.o: %.c
     @echo $(notdir $<)
     $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d $(CFLAGS) -c $< -o $@

  9. #---------------------------------------------------------------------------------
    %.o: %.s
     @echo $(notdir $<)
     $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@
  10. #---------------------------------------------------------------------------------
    %.o: %.S
     @echo $(notdir $<)
     $(CC) -MMD -MP -MF $(DEPSDIR)/$*.d -x assembler-with-cpp $(ASFLAGS) -c $< -o $@
  11. #---------------------------------------------------------------------------------
    # canned command sequence for binary data
    #---------------------------------------------------------------------------------
    define bin2o
     bin2s $< | $(AS) $(ARCH) -o $(@)
     echo "extern const u8" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(<F) | tr . _)`.h
     echo "extern const u8" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(<F) | tr . _)`.h
     echo "extern const u32" `(echo $(<F) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(<F) | tr . _)`.h
    endef

 각 파일의 확장자에 따라서 어떻게 빌드해야 할지에 대한 구체적인 내용이 나와있다. 그럼 이 make 파일을 이용하여 build를 하면 컴파일 되고 적당히 링크되어 1차적으로 elf 파일이 생성될 것이다.

 컴파일해서 나온 .o 파일은 별다른 의문이 없지만 링크되어 나온 elf 파일은 조금 생각해 봐야 한다. objcopy로 elf 파일을 binary 형태로 변화시키는데, 그렇다면 elf 파일도 NDS 롬 파일 형태에 맞도록 뭔가 달라져야 할 것이 아닌가? 링크는 LDFLAG 옵션에 들어있는 ds_arm9.specs 을 참고하도록 되어있으니 Linker 쪽을 살펴보자.

 

2. 링커 스크립트(Linker Script) 분석

 ds_arm9.specs 파일은 devkitARM\arm-eabi\lib 폴더 아래에서 살펴볼 수 있다.

  1. %rename link                old_link
    %rename link_gcc_c_sequence old_gcc_c_sequence
  2. *link_gcc_c_sequence:
    %(old_gcc_c_sequence) --start-group -lsysbase -lc --end-group
  3. *link:
    %(old_link) -T ds_arm9.ld%s
  4. *startfile:
    ds_arm9_crt0%O%s crti%O%s crtbegin%O%s

 구체적인 내용은 확실히 모르겠지만 일단 ds_arm9.ld 파일을 참조하고 ds_arm9_crt, crti, crtbegin 으로 시작하는 파일과 관계가 있음을 유추할 수 있다. ds_arm9.ld 파일을 한번 살펴보면 아래와 같다.

  1. OUTPUT_FORMAT("elf32-littlearm", "elf32-bigarm", "elf32-littlearm")
    OUTPUT_ARCH(arm)
    ENTRY(_start)
  2. MEMORY {
  3.  rom : ORIGIN = 0x08000000, LENGTH = 32M
     ewram : ORIGIN = 0x02000000, LENGTH = 4M - 4k
     dtcm : ORIGIN = 0x0b000000, LENGTH = 16K
     itcm : ORIGIN = 0x01000000, LENGTH = 32K
    }
  4. __itcm_start = ORIGIN(itcm);
    __ewram_end = ORIGIN(ewram) + LENGTH(ewram);
    __eheap_end = ORIGIN(ewram) + LENGTH(ewram);
    __dtcm_start = ORIGIN(dtcm);
    __dtcm_top = ORIGIN(dtcm) + LENGTH(dtcm);
    __irq_flags = __dtcm_top - 0x08;
    __irq_vector = __dtcm_top - 0x04;
  5. __sp_svc = __dtcm_top - 0x100;
    __sp_irq = __sp_svc - 0x100;
    __sp_usr = __sp_irq - 0x100;
  6. SECTIONS
    {
     .init :
     {
      __text_start = . ;
      KEEP (*(.init))
      . = ALIGN(4);  /* REQUIRED. LD is flaky without it. */
      } >ewram = 0xff
  7.  .plt : { *(.plt) } >ewram = 0xff
  8.  .text :   /* ALIGN (4): */
     {
      *(EXCLUDE_FILE (*.itcm*) .text)
  9.   *(.text.*)
      *(.stub)
      /* .gnu.warning sections are handled specially by elf32.em.  */
      *(.gnu.warning)
      *(.gnu.linkonce.t*)
      *(.glue_7)
      *(.glue_7t)
      . = ALIGN(4);  /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff
  10.  .fini           :
     {
      KEEP (*(.fini))
     } >ewram =0xff
  11.  __text_end = . ;
  12.  .rodata :
     {
      *(.rodata)
      *all.rodata*(*)
      *(.roda)
      *(.rodata.*)
      *(.gnu.linkonce.r*)
      SORT(CONSTRUCTORS)
      . = ALIGN(4);   /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff
  13.   .ARM.extab   : { *(.ARM.extab* .gnu.linkonce.armextab.*) } >ewram
       __exidx_start = .;
      .ARM.exidx   : { *(.ARM.exidx* .gnu.linkonce.armexidx.*) } >ewram
       __exidx_end = .;
      /* Ensure the __preinit_array_start label is properly aligned.  We
         could instead move the label definition inside the section, but
         the linker would then create the section even if it turns out to
         be empty, which isn't pretty.  */
      . = ALIGN(32 / 8);
      PROVIDE (__preinit_array_start = .);
      .preinit_array     : { KEEP (*(.preinit_array)) } >ewram = 0xff
      PROVIDE (__preinit_array_end = .);
      PROVIDE (__init_array_start = .);
      .init_array     : { KEEP (*(.init_array)) } >ewram = 0xff
      PROVIDE (__init_array_end = .);
      PROVIDE (__fini_array_start = .);
      .fini_array     : { KEEP (*(.fini_array)) } >ewram = 0xff
      PROVIDE (__fini_array_end = .);
  14.  .ctors :
     {
     /* gcc uses crtbegin.o to find the start of the constructors, so
      we make sure it is first.  Because this is a wildcard, it
      doesn't matter if the user does not actually link against
      crtbegin.o; the linker won't look for a file to match a
      wildcard.  The wildcard also means that it doesn't matter which
      directory crtbegin.o is in.  */
      KEEP (*crtbegin.o(.ctors))
      KEEP (*(EXCLUDE_FILE (*crtend.o) .ctors))
      KEEP (*(SORT(.ctors.*)))
      KEEP (*(.ctors))
      . = ALIGN(4);   /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff
  15.  .dtors :
     {
      KEEP (*crtbegin.o(.dtors))
      KEEP (*(EXCLUDE_FILE (*crtend.o) .dtors))
      KEEP (*(SORT(.dtors.*)))
      KEEP (*(.dtors))
      . = ALIGN(4);   /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff
  16.  .eh_frame :
     {
      KEEP (*(.eh_frame))
      . = ALIGN(4);   /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff
  17.  .gcc_except_table :
     {
      *(.gcc_except_table)
      . = ALIGN(4);   /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff
     .jcr            : { KEEP (*(.jcr)) } >ewram = 0
     .got            : { *(.got.plt) *(.got) *(.rel.got) } >ewram = 0
  18.  .ewram ALIGN(4) :
     {
      __ewram_start = ABSOLUTE(.) ;
      *(.ewram)
      *ewram.*(.text)
      . = ALIGN(4);   /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff

  19.  .data ALIGN(4) :
     {
      __data_start = ABSOLUTE(.);
      *(.data)
      *(.data.*)
      *(.gnu.linkonce.d*)
      CONSTRUCTORS
      . = ALIGN(4);
      __data_end = ABSOLUTE(.) ;
     } >ewram = 0xff

  20.  __dtcm_lma = . ;
  21.  .dtcm __dtcm_start : AT (__dtcm_lma)
     {
      *(.dtcm)
      *(.dtcm.*)
      . = ALIGN(4);
      __dtcm_end = ABSOLUTE(.);
     } >dtcm = 0xff

  22.  __itcm_lma = __dtcm_lma + SIZEOF(.dtcm);
  23.  .itcm __itcm_start : AT (__itcm_lma)
     {
      *(.itcm)
      *itcm.*(.text)
      . = ALIGN(4);
      __itcm_end = ABSOLUTE(.);
     } >itcm = 0xff
     
     .sbss __dtcm_end :
     {
      __sbss_start = ABSOLUTE(.);
      __sbss_start__ = ABSOLUTE(.);
      *(.sbss)
      . = ALIGN(4);    /* REQUIRED. LD is flaky without it. */
      __sbss_end = ABSOLUTE(.);
     } >dtcm

  24.  __bss_lma = __itcm_lma + SIZEOF(.itcm) ;
     __appended_data = __itcm_lma + SIZEOF(.itcm) ;
     .bss __bss_lma : AT (__bss_lma)
     {
      __bss_start = ABSOLUTE(.);
      __bss_start__ = ABSOLUTE(.);
      *(.dynbss)
      *(.gnu.linkonce.b*)
      *(.bss*)
      *(COMMON)
      . = ALIGN(4);    /* REQUIRED. LD is flaky without it. */
      __bss_end = ABSOLUTE(.) ;
      __bss_end__ = __bss_end ;
     } >ewram

  25.  _end = . ;
     __end__ = . ;
     PROVIDE (end = _end);
  26.  /* Stabs debugging sections.  */
     .stab 0 : { *(.stab) }
     .stabstr 0 : { *(.stabstr) }
     .stab.excl 0 : { *(.stab.excl) }
     .stab.exclstr 0 : { *(.stab.exclstr) }
     .stab.index 0 : { *(.stab.index) }
     .stab.indexstr 0 : { *(.stab.indexstr) }
     .comment 0 : { *(.comment) }
     /* DWARF debug sections.
      Symbols in the DWARF debugging sections are relative to the beginning
      of the section so we begin them at 0.  */
     /* DWARF 1 */
     .debug          0 : { *(.debug) }
     .line           0 : { *(.line) }
     /* GNU DWARF 1 extensions */
     .debug_srcinfo  0 : { *(.debug_srcinfo) }
     .debug_sfnames  0 : { *(.debug_sfnames) }
     /* DWARF 1.1 and DWARF 2 */
     .debug_aranges  0 : { *(.debug_aranges) }
     .debug_pubnames 0 : { *(.debug_pubnames) }
     /* DWARF 2 */
     .debug_info     0 : { *(.debug_info) }
     .debug_abbrev   0 : { *(.debug_abbrev) }
     .debug_line     0 : { *(.debug_line) }
     .debug_frame    0 : { *(.debug_frame) }
     .debug_str      0 : { *(.debug_str) }
     .debug_loc      0 : { *(.debug_loc) }
     .debug_macinfo  0 : { *(.debug_macinfo) }
     /* SGI/MIPS DWARF 2 extensions */
     .debug_weaknames 0 : { *(.debug_weaknames) }
     .debug_funcnames 0 : { *(.debug_funcnames) }
     .debug_typenames 0 : { *(.debug_typenames) }
     .debug_varnames  0 : { *(.debug_varnames) }
     .stack 0x80000 : { _stack = .; *(.stack) }
     /* These must appear regardless of  .  */
    }

 위에서 보면 링크에 관련된 온갖 정보들이 다 들어있음을 알 수 있다. 주의깊게 볼 부분은 파란색 부분인데, 이 부분의 주소값들이 다 NDS의 메모리맵과 관계가 있다. 링크 스크립트에 대한 내용은 06 링커 스크립트(Linker Scrpit) 페이지를 참조하도록 하고 위에서 계속 반복해서 나오는 섹션에 대한 정보만 간단히 알아보자.

  1.  .text :   /* ALIGN (4): */
     {
      *(EXCLUDE_FILE (*.itcm*) .text)
  2.   *(.text.*)
  3.   *(.stub)
      /* .gnu.warning sections are handled specially by elf32.em.  */
      *(.gnu.warning)
      *(.gnu.linkonce.t*)
      *(.glue_7)
      *(.glue_7t)
      . = ALIGN(4);  /* REQUIRED. LD is flaky without it. */
     } >ewram = 0xff

  >ewram = 0xff 의 뜻은 .text 영역의 섹션을 위에서 정의했던 ewram 영역에 밀어넣되, .text 섹션의 값은 0xff 값으로 초기화 하라는 것이다. 즉 ewram 영역에 .text 영역이 생길꺼고 그 영역은 0xff로 체워진다. sbss 및 bss 영역 역시 ewram에 위치하는 걸 봐서 데이터도 모두 ewram 영역에 위치하므로, 너무 큰 배열이나 값들의 연속은 메모리를 지나치게 사용할 우려가 있으므로 주의해야 한다. 

 위에서 보면 모든 많은 섹션들이 >ewram으로 표시된 것을 보아 거의 ewram 영역인 메인 메모리에 올라감을 알 수 있다. 그렇다면 ROM과 같은 GBA 카드리지 영역은 안쓰는 걸까? 현재( 2007/08/16 21:59:18 )까지 분석된 결과로는 거의 안쓰이는 것 같다.

 

3.map 파일 분석

 위에서 보았던 링크 스크립트 파일을 이용하여 링커가 elf 파일을 생성하게 된다. 이때 생성된 elf 파일에 구체적인 데이터는 map 파일을 열어보면 알 수 있다. 아래는 map 파일의 내용을 간단히 추린 것이다.

  1.  Memory Configuration
  2. Name             Origin             Length             Attributes
    rom              0x08000000         0x02000000
    ewram            0x02000000         0x003ff000
    dtcm             0x0b000000         0x00004000
    itcm             0x01000000         0x00008000
    *default*        0x00000000         0xffffffff
  3. Linker script and memory map
  4.                 0x01000000                __itcm_start = 0x1000000
                    0x023ff000                __ewram_end = 0x23ff000
                    0x023ff000                __eheap_end = 0x23ff000
                    0x0b000000                __dtcm_start = 0xb000000
                    0x0b004000                __dtcm_top = 0xb004000
                    0x0b003ff8                __irq_flags = (__dtcm_top - 0x8)
                    0x0b003ffc                __irq_vector = (__dtcm_top - 0x4)
                    0x0b003f00                __sp_svc = (__dtcm_top - 0x100)
                    0x0b003e00                __sp_irq = (__sp_svc - 0x100)
                    0x0b003d00                __sp_usr = (__sp_irq - 0x100)
  5. .init           0x02000000      0x22c
                    0x02000000                __text_start = .
     *(.init)
    .init          0x02000000      0x220 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/../../../../arm-eabi/lib/thumb/ds_arm9_crt0.o
                    0x02000000                _start
     .init          0x02000220        0x4 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crti.o
                    0x02000220                _init
     .init          0x02000224        0x8 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crtn.o
                    0x0200022c                . = ALIGN (0x4)
  6. .plt
     *(.plt)
  7. .text           0x02000240    0x1cae0
     *(EXCLUDE_FILE(*.itcm*) .text)
     .text          0x02000240        0x0 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/../../../../arm-eabi/lib/thumb/ds_arm9_crt0.o
     .text          0x02000240        0x0 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crti.o
     .text          0x02000240       0x70 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crtbegin.o
     .text          0x020002b0      0x178 2DRaster.o
                    0x020002b0                DrawLine(void*, int, int, int, int, unsigned short)
                    0x0200041c                DrawPixel(void*, int, int, unsigned short)
                    0x02000370                DrawBox(void*, int, int, int, int, unsigned short, bool)
     .text          0x02000428      0x124 BootStub.o
                    0x020004d4                InitVideoMode()
                    0x02000428                SetIrq()
                    0x02000474                IrqHandler()
  8. ........................
  9. .data           0x0201ff64    0x138e0
                    0x0201ff64                __data_start = <code 342> (.)
     *(.data)
     .data          0x0201ff64        0x0 d:/ndsl_develo
  10.  .data          0x0201ff64        0x4 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crtbegin.o
                    0x0201ff64                __dso_handle
     .data          0x0201ff68        0x0 2DRaster.o
     .data          0x0201ff68        0x0 BootStub.o
     .data          0x0201ff68        0x0 DC.o
     .data          0x0201ff68        0x0 DrawingWindow.o
     .data          0x0201ff68        0x0 FileManager.o
     .data          0x0201ff68    0x12c20 Font.o
                    0x0201ff68                g_vusHanFont
  11.  ........................
  12.  .bss            0x020339d4    0x18e68 load address 0x020339d4
                    0x020339d4                __bss_start = <code 342> (.)
                    0x020339d4                __bss_start__ = <code 342> (.)
     *(.dynbss)
     *(.gnu.linkonce.b*)
     *(.bss*)
     .bss           0x020339d4        0x0 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/../../../../arm-eabi/lib/thumb/ds_arm9_crt0.o
     .bss           0x020339d4        0x0 d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crti.o
     .bss           0x020339d4       0x1c d:/ndsl_develop/devkitpro/devkitarm/bin/../lib/gcc/arm-eabi/4.1.1/thumb/crtbegin.o
     .bss           0x020339f0        0x0 2DRaster.o
     .bss           0x020339f0        0x3 BootStub.o
                    0x020339f2                g_ucIPCCount
                    0x020339f1                g_ucKeysCount
                    0x020339f0                g_ucVBlankCount
     .bss           0x020339f3        0x0 DC.o
     .bss           0x020339f3        0x0 DrawingWindow.o
     .bss           0x020339f3        0x0 FileManager.o
     .bss           0x020339f3        0x0 Font.o
     .bss           0x020339f3        0x0 InformationWindow.o
     *fill*         0x020339f3        0x1 00
     .bss           0x020339f4    0x18568 Main.o
                    0x020339f4                g_clInformationWindow
                    0x02033b60                g_clBackGroundWindow


map 파일의 내용들을 보면 위와 같은 내용을 확인할 수 있는다. 우리가 작성한 코드는 .text 영역에 있으며, 우리가 사용한 변수들은 .bss 영역에 있는 것을 확인할 수 있다. 그리고 초기화 된 데이터 같은 경우(g_vusHanFont)는 .data에 존재하는 것을 발견할 수 있다.

 

 

4.Objcopy 분석

 일단 컴파일 및 링크의 결과로 elf 파일이 생성되었음을 알았다. 그럼 objcopy를 통해 binary 파일로 만들면 어떤 일이 발생하는 걸까? objcopy에 대한 내용은 http://www.gnu.org/software/binutils/manual/html_chapter/binutils_3.html 에서 찾을 수 있다. 내용을 조금 살피다 보면 -O binary 옵션에 대한 내용을 찾을 수 있는데 아래와 같다.

objcopy can be used to generate a raw binary file by using an output target of `binary' (e.g., use `-O binary'). When objcopy generates a raw binary file, it will essentially produce a memory dump of the contents of the input object file. All symbols and relocation information will be discarded. The memory dump will start at the load address of the lowest section copied into the output file.

 각 섹션들의 덤프를 해준다는 이야기 같다. 제일 Lowest한 주소부터 그대로 섹션 덤프를 해준다는데... 확인을 해보자.

 KKAMAGUI NOTEPAD의 .elf 파일 용량은 500Kbyte이다. 하지만 .arm9 파일 용량은 207Kbyte이다. 사이즈가 상당히 줄었음을 알 수 있다. 그럼 데이터는 어떻게 구성되어있는 걸까?

 

4.1 objdump로 .elf 분석 및 .arm9 파일 분석

 elf 파일의 정보는 devkitPro를 설치하면 있는 objdump.exe 프로그램을 이용하면 볼 수 있다. 이 프로그램과 파일의 정보를 hex로 보여주는 울트라 에디트와 같은 Hex Editor를 사용해서 파일 내용을 비교해 보면 된다. 여기서 Hex Editor는 HxD를 이용하기로 한다. HxD는 http://mh-nexus.de/hxd/ 페이지에서 받을 수 있다.

 아래는 HxD로 Notepad.arm9 파일을 연 화면이다.

 

HxD1.PNG

<HxD의 .arm9 분석 화면>

  일단 대충 이런 내용이구나 정보만 알아놓고 objdump 프로그램을 이용해서 Notepad.elf 파일의 섹션 정보를 보자.

 

HxD2.PNG

<objdump의 실행화면>

 LMAVMA의 의미에 대해서 잠깐 알아보자.

  • LMA(Loaded Memory Address) :  실제 elf 파일이 메모리에 로드되는 위치. VMA와 일반적으로 동일
  • VMA(Virtual Memory Address) : 프로그램이 실행될 때 메모리의 위치. ROM 같은 경우는 메모리에 로드된 후에 실행될 위치로 코드가 이동되어야 하므로 일반적으로 다름.

 

4.2 TCM의 위치 및 메모리 로드 분석

  여기서는 ITCM 영역이 링커 스크립트에 의해 여러가지 조건 때문에 제일 앞쪽(0번 섹션)에 위치하지 못하여 어긋난 것 같다. NDS 롬 헤더에 TCM에 대한 정보 필드가 없고, objcopy 프로그램이 단순히 섹션의 내용을 VMA 순서대로 덤프를 하는데... 저 섹션들이 의미가 있을까?

 "답은 의미가 있다"  이다.

 \devkitPro\devkitARM\arm-eabi\lib 폴더에 가면 ARM9의 CRT 코드를 찾을 수 있다. ds_arm9_crt0.s 파일이 그것인데, 아래와 같은 내용을 담고 있다.

  1.  ldr r1, =__itcm_lma  @ Copy instruction tightly coupled memory (itcm section) from LMA to VMA (ROM to RAM)
     ldr r2, =__itcm_start
     ldr r4, =__itcm_end
     bl CopyMemCheck
  2.  ldr r1, =__dtcm_lma @ Copy data tightly coupled memory (dtcm section) from LMA to VMA (ROM to RAM)
     ldr r2, =__dtcm_start
     ldr r4, =__dtcm_end
     bl CopyMemCheck

 crt0 코드는 링크될때 가장 앞부분에 위치하는 코드로써 홈브루의 초기화 및 기타 작업을 담당하는 것 같다. 위에서 보면 __itcm 같은 변수를 사용하는 것을 알 수 있는데, 이 값들은 링크 스크립트 파일(ds_arm9.ld)을 보면 잘 정의되어있다. 그러므로 binary로 생성될때는 정확한 위치에 있지 않지만, 실행하면서 해당 위치에 옮겨지기때문에 가능하다.

 

4.3 .elf의 Hex 데이터 분석

그럼 이제 첫번째 섹션 .init의 시작위치를 알았으므로 이제 HxD 프로그램을 이용해서 .elf 파일의 0x8000 영역으로 옮겨보자.

HxD3.PNG

 위의 .arm9 파일과 동일함을 알 수 있다.

 다른 섹션들도 따라가보면 똑같음을 볼 수 있고, 이로서 objcopy의 -O binary 옵션이 하는 일은 섹션의 VMA 순서에 따라서 첫번째 VMA 주소를 시작으로 메모리 내용을 덤프하는 것과 같음을 알 수 있다.

 .arm9파일의 내용은 단순한 코드와 데이터의 집합이다. 

 

5. NDS Tool 분석

 이제 마지막 단계만 남았다. .arm9 파일을 이용해서 .nds 파일을 생성하는 단계이다. NDS 파일의 헤더 정보에 대해서는 05 NDS 롬(ROM) 파일 포맷(Format) 분석를 살펴보자. 해당 페이지의 결과를 보면 단순히 헤더를 붙이고 ARM9과 ARM7 코드를 붙여넣는 작업이 전부임을 알 수 있다. ㅜ_ㅜ... 어찌나 간단한지...

 참고삼아 ndstool.exe 를 이용하여 분석한 Notepad.nds 파일 정보를 붙여넣었다.

  1. D:\ndsl_develop\MyProject\Notepad-업로드용>ndstool -i Notepad.nds
    Nintendo DS rom tool 1.33 - Jan 28 2007 21:02:20
    by Rafael Vuijk, Dave Murphy,  Alexei Karpenko
    Header information:
    0x00    Game title                      .
    0x0C    Game code                       ####
    0x10    Maker code
    0x12    Unit code                       0x00
    0x13    Device type                     0x00
    0x14    Device capacity                 0x01 (2 Mbit)
    0x15    reserved 1                      000000000000000000
    0x1E    ROM version                     0x00
    0x1F    reserved 2                      0x04
    0x20    ARM9 ROM offset                 0x200
    0x24    ARM9 entry address              0x2000000
    0x28    ARM9 RAM address                0x2000000
    0x2C    ARM9 code size                  0x33C74
    0x30    ARM7 ROM offset                 0x34000
    0x34    ARM7 entry address              0x37F8000
    0x38    ARM7 RAM address                0x37F8000
    0x3C    ARM7 code size                  0x12B8
    0x40    File name table offset          0x35400
    0x44    File name table size            0x9
    0x48    FAT offset                      0x35600
    0x4C    FAT size                        0x0
    0x50    ARM9 overlay offset             0x0
    0x54    ARM9 overlay size               0x0
    0x58    ARM7 overlay offset             0x0339d4

    0x5C    ARM7 overlay size               0x0
    0x60    ROM control info 1              0x007F7FFF
    0x64    ROM control info 2              0x203F1FFF
    0x68    Icon/title offset               0x0
    0x6C    Secure area CRC                 0x0000 (-, homebrew)
    0x6E    ROM control info 3              0x051E
    0x70    ARM9 ?                          0x0
    0x74    ARM7 ?                          0x0
    0x78    Magic 1                         0x00000000
    0x7C    Magic 2                         0x00000000
    0x80    Application end offset          0x00035600
    0x84    ROM header size                 0x00000200
    0xA0    ?                               0x4D415253
    0xA4    ?                               0x3131565F
    0xA8    ?                               0x00000030
    0xAC    ?                               0x53534150
    0xB0    ?                               0x00963130
    0x15C   Logo CRC                        0x9E1A (OK)
    0x15E   Header CRC                      0xAD78 (OK)

  2. D:\ndsl_develop\MyProject\Notepad-업로드용>

  실제 .arm9 파일의 크기는 0x339D4의 크기인데, NDS Rom 파일이 생성될때 해당 롬의 크기가 0x2A0 만큼 더 큰 것을 보아 약간의 공간이 더 추가된 듯 하다. 그럼 어떤 데이터가 추가된 것을까? 아래는 .arm9 파일과 .nds 파일의 마지막 부분의 내용을 비교한 것이다.

HxD4.PNG

 

HxD5(1).PNG

<.arm9 파일(좌측)과 .nds 파일(우측) 내용>

 그림이 좀 심하게 일그러졌는데, 첨부파일의 큰 그림을 참조하도록 하고, 여기서 알 수 있는 사실은 추가된 0x2A0 공간만큼 0으로 체워져있다는 사실이다. 따라서 .arm9 바이너리 파일을 그대로 덤프한다는 사실에는 변함이 없다.

 

마치면서...

 NDS 롬(ROM) 파일 생성을 분석하는데, 자그마치 이틀이나 걸렸다. 상당히 복잡했고 이것 저것 볼 것도 많았지만, 확실히 NDS 파일에 대해서 알 수 있는 좋은 기회였다. 결국 NDS 파일은 ARM9과 ARM7 코드와 데이터를 포함하는 간단한 구조였던 것이다.

 이제 NDS는 나의 장난감이 된 것이나 다름없다. >ㅁ<)/~ 기다려라~ NDS야. 하하하하핫~~!!!

 

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

00 NDS 홈브루(Homebrew) - KKAMAGUI Notepad

원본 : http://kkamagui.springnote.com/pages/416259

 

들어가기 전에...

 

1.소개

 내가 만든 간단한 그림판 형식의 메모장 프로그램이다. 메모를 생성하고 삭제하는 기능을 가지고 있으며, 메모 보기 기능도 갖추고 있다.

 libfat를 사용하여 디스크에서 직접 데이터 파일을 사용하며 데이터 파일을 백업하여 데이터를 보존할 수 도 있다.

 사실 KKAMAGUI NOTEPAD 프로그램을 만들면서 NOTEPAD 프로그램을 개발하는 시간보다 윈도우 MFC 구조와 비슷하게 클래스를 구성하고 코딩하는 시간이 더 많이 걸렸다. UI에 전혀 소질이 없는지라... 출력물은 좀 엉망이지만... 조금만 손보면 그럴듯하게 바꿀 수도 있을듯....

 아래는 실행한 화면이다.

notepad11.PNG   NOTEPAD2.PNG   NOTEPAD1.PNG

<시작화면(좌측)과 메모를 입력하는 화면(가운데), 그리고 메모를 보는 화면(우측)>

 

 

2.프로그램 특징

 KKAMAGUI NOTEPAD는 아래와 같은 특징을 가지고 있다.

  • libfat 사용
    • libfat 파일을 수정하여 FAT 파일 시스템으로 포맷된 디스크에 접근하여 데이터 파일을 생성하고 수정함
    • 수정 방법은 01 libfat 업그레이드 부분을 참조
  • 데이터 파일 용량 최소화 
    • 화면 데이터 저장 시, 비트마스크를 이용하여 한 바이트에 8pixel 정보를 저장
    • 256 * 192 => 48Kbyte의 기존 사이즈를 6Kbyte로 줄임
  • 한글 출력
    • 수정한 한글 출력 루틴 및 자작 한글 폰트(굴림체 16x16 pixel 사용) 사용
    • 출처는 네이버 NDS 개발 까페
    • 한글 폰트 제작 및 출력에 관한 부분은 01 NDS 한글 출력 라이브러리 부분을 참조
  • Windows의 MFC 형식의 구조와 비슷한 윈도우 라이브러리 제작 및 사용
    • 자작한 클래스 사용
    • Windows manager class
    • Window base class/DC class
    • Z-Order 기능 지원
    • 환경적 제약사항(Single Task)으로 Message Loop 제거, 해당 윈도우의 Message 처리 루틴을 직접 호출하도록 변경
    • 자세한 내용은 02 NDS 윈도우 시스템 문서를 참조하자.

 

 

3.프로그램 기능

 KKAMAGUI NOTEPAD의 기능은 아래와 같다.

 

3.1 메모 입력 및 입력 취소 기능

  • 연필 및 지우개 선택 기능
    • 연필 모드로 화면 그리기 가능
    • 지우개 모드로 화면 지우기 가능
  • "지우개" 버튼 : 상단의 화면에 연필/지우개 표시
  • “확인” 버튼 : 메모 저장
  • “취소” 버튼 : 변경 사항 입력된 내용 버림

 Notepad4.PNG   ===>   Notepad6.PNG

 Notepad3.PNG   ===>  Notepad5.PNG

<연필 모드(좌측)과 지우개 모드(우측)>

 

 

3.2 메모 보기 및 삭제 기능

  • 기록된 메모 보기 : 상단 화면에 몇변째 메모인지가 표시된다.
  • “확인” 버튼 : 메뉴로 돌아감
  • “삭제” 버튼 : 메모 삭제
  • “다음/이전” 버튼 : 다음/이전 메모 보기

 

 Notepad8.PNG  ===>  Notepad10.PNG

 Notepad7.PNG  ===>  Notepad9.PNG

<첫번째 메모 화면(좌측)과 두번재 메모 화면(우측)>

 

 

3.3 특이사항

 메모장을 구현할 때 프로토타입(Prototype)을 만드는데 너무 열중하다 보니, 데이터 파일에 데이터를 저장하는 방식이 조금 이상하다.

 데이터를 저장할때 한바이트의 플래그 바이트와 나머지 6Kbyte 화면정보를 연속해서 저장하는 방식으로 동작한다. 데이터가 써지면 플래그 바이트를 0x01로 설정하고 메모가 지워지면 플래그 바이트를 0x00으로 설정하여 빈 공간임을 표시하는 것이다.

 메모의 추가 및 삭제가 여러번 반복되면 중간 중간에 플래그 Byte가 0x00 인 부분이 생기게된다. 메모가 추가되면 플래그 Byte 중에 0x00으로 설정된 제일 첫번째 것을 찾아서 해당 위치에 데이터를 추가하고 플래그 바이트를 0x01로 설정한다.

 이렇게 동작 하기 때문에 메모가 데이터 파일에 저장되는 순서하고 메모가 기록된 시간적 순서하고 일치하지 않는 문제가 있는데, 메모를 빼먹지는 않으므로(ㅡ,.ㅡ;;;) 그렇다는 것만 알고 넘어가자.

 

 

4.컴파일, 링크 그리고 실행

  Programmer's Notepad 2 프로그램을 이용할 시에는 Notepad.pnproj 파일을 열어서 컴파일 및 링크하면 되고, 콘솔(cmd.exe)을 사용하는 경우에는 make를 입력하거나 makefile.bat를 더블클릭하여 실행하면 된다. 자세한 방법은 00 NDS 개발 킷(Devkit Pro) 설치 문서를 참조하자.

 컴파일 및 링크가 정상적으로 끝나면 Notepad.nds 파일이 생성된다. 에뮬레이터나 디스크에 넣어서 직접 NDS에서 실행하면 된다.

 

 

5.첨부

 

 

이 글은 스프링노트에서 작성되었습니다.

 티스토리로 옮겨오고나서 제일 좋은 점이 자바 스크립트가 먹는다는 것입니다. ^^ 아는 후배가 티스토리로 옮겨오자마자 문법 하일라이트(Syntax Highlight)를 해주는 자바 스크립트를 올렸더군요(어찌나 기특하던지 ㅎㅎ). 자바 스크립트 소스코드는 http://code.google.com/p/syntaxhighlighter/ 에서 받을 수 있습니다.

 소스코드를 올리면 아래와 같이 이쁘게 나옵니다. ^^;;;


 방법은 html 모드로 전환한 뒤 아래와 같이 쓰거나


 아래와 같이 사용하면 됩니다. ^^


 자세한 설치 방법은 아래의 사이트를 참고하시면 됩니다. ^^
원문 : http://gyuha.tistory.com/?_best_tistory=best_blogger3


ps) javascript 파일을 모두 하나로 뭉쳐서 shTotal.js 파일로 만든다음 </body> 바로 위에 아래와 같이 넣어도 된다.


사용자 삽입 이미지

내가 작성한 코드


 늦은 시간 지인과 채팅을 하다가 지인께서 주석을 달지 않는다는 말에 솔깃하여 좋은 코드와 주석에 대해서 많은 이야기를 나누었습니다. 물론 코드와 주석의 관계는 예전부터 말이 많았었고, 오죽하면 MS에서 낸 Code Complete(책)에서도 코드와 주석에 대해서 한 챕터(Chapter)를 할애해서 설명했겠습니까? ^^;;;

 지인께서는 주석이 사실 거의 필요 없고 코드를 간결하게 짜면 코드가 모든걸 설명해 줄꺼라 말씀하셨지만....
저는 주석이 아예 필요없다고 생각하시는 것은 아닌지... 혹시나 하는 마음에 여러 이야기를 나누어봤는데, 리눅스 커널 프로젝트를 예로 들면서 주석이 별로 없어도 코드를 간결하게 작성하면 된다고 하셨습니다. 제가 리눅스 커널을 깊게 보지는 않았지만 주석이 거의 없었던 듯....(아닐수도 있습니다. ^^;;;)

 물론 주석이 가지는 문제가 많습니다만은, 저는 코드에 주석을 풍부하게 달아야 한다고 생각하기에 여러 딴지를 걸었는데요, 글쎄요... 아무리 프로그램을 간단하게 짜고 코드가 모든 것을 설명해 줄 수 있게 의미있는 함수명을 짓고 변수명을 붙인다 하더라도...
 이것은 어디까지나 지극히 "개인적인" 기준이라고 생각합니다. 만약 후임이나 혹은 팀으로 작업을 한다고 했을 때 소스코드 분석을 먼저하게 될텐데... 주석하나 없는 코드를 볼때의 임펙트(??)는 둘째치고 주석 한줄(~~해서 ~~하여 ~~한다)이면 "알수있는" 부분을 코드 전체를 뒤져서 "알아내야" 하는 시간적인 낭비는 무시 못한다고 생각합니다. ^^;;;;
 특히나 생각이 꼬여(??)있으신 분이라면 아무리 쉽게 모듈을 나누고 정리하더라도 다른 사람이 쉽게 이해하긴 힘들겠지요.

 저는 코딩할때 주석을 꽤나 많이 다는 편인데, 다른 분들은 어떻게 생각하실지 궁금하군요. (갑자기 그동안 릴리즈 해놓은 많은 소스코드가 마음에 걸려서 뜨끔하다는... ^^;;;;)

 코드에 주석 많이 다세요?

참고. NDS 동영상 인코딩(BatchDpg)

 원문 :  http://kkamagui.springnote.com/pages/492909


들어가기 전에...

0.시작하면서...

 NDS로 동영상을 보려면 문쉘과 DPG 파일 포맷으로 인코딩해주는 툴이 필요하다. 물론 NDS의 사양이 그렇게 좋지 않기 때문에 인코딩시에 질을 많이 떨어뜨려야 하는 문제가 있지만... 나름 볼만하므로 통근시간이 길거나 학교까지 거리가 먼 사람들은 인코딩을 시도해 볼만하다.


 DPG 파일 포맷은 간단한 헤더와 MP2 Audio 그리고 MPEG1 Video 파일로 구성된다. 쉽게 이야기하면 오디오 파일 따로 비디오 파일 따로 생성하여 그냥 묶어놓은 것 뿐이다. 프로그램에 능한 사람이라면 인코딩 라이브러리를 사용해서 인코딩하여 DPG 파일을 생성하는 것도 가능할 것이다. 일반인들은 인코딩 프로그램을 사용하면 되는데, 많이 쓰는 인코딩 툴은 BatchDPG가 있다.

 BatchDPG를 이용해서 인코딩 하는 방법을 알아보도록 하자.


1.BatchDPG 설치

 BatchDPG 파일은 구글에서 검색하면 많이 나오는데, 거의 영문판이다. 다행이도 한글화시켜놓은 프로그램이 있으니 BatchDPG 1.2K 버전이다. 현재( 2007/09/23 20:16:34 )까지 1.3 Beta 버전까지 나와있는데, 구버전을 쓰기가 좀 그렇다고 느끼는 사람은  BatchDPG12KNew.zip 를 받으면 된다. 1.2K 버전에 1.3 Beta 버전의 Libary를 덮어씌운 버전이다.


 BatchDPG가 정상적으로 동작하려면 추가적으로 아래와 같은 것들이 더 필요하다.


2.BatchDPG 사용

 위의 두 프로그램을 다운받아 설치한 후 BatchDPG를 실행하면 아래와 같은 화면이 표시된다.

 화면.PNG


 사용방법은 아래와 같다.

  1. 동영상과 자막을 설정하고 맨 아래에 추가 버튼을 이용하여 작업을 추가한다.
  2. 작업할 동영상을 계속 반복하여 추가한다.
  3. 작업 추가가 끝났으면 맨 아래에 실행 버튼을 클릭하여 인코딩 작업을 수행한다.

 작업이 완료되면 해당 폴더에 DPG 파일이 생성된다.


3.마치면서...

 간단히 DPG로 인코딩하는 방법을 알아보았다. 이제 DPG의 세계로 빠져보자. @0@)/~~!!



이 글은 스프링노트에서 작성되었습니다.

참고. 3 in 1 Expansion Pack 사용법

원문 :  http://kkamagui.springnote.com/pages/527617

 

들어가기 전에...

 

0.시작하면서...

 3 in 1 Expansion Pack은 NDS의 기능을 확장해주는 확장 팩 정도로 생각하면 되고 Slot 2 (GBA 팩을 꼽는 부분)에 삽입하도록 되어있다. Expansion Pack에 대한 자세한 내용은 http://wiki.gbatemp.net/index.php?title=3_in_1_Expansion_Pack_for_EZ-Flash_V 에서 참고할 수 있다.

 

사진.jpg

<Expansion Pack>

 

1.주요기능 

 Expansion Pack의 주요기능은 3가지이다. 

  • 진동 기능 
  • GBA 게임용 저장공간(NOR Flash) 
  • RAM 확장(16MByte). DS Browser에서 사용 가능

 

2.하드웨어 구성 

  • 256 Mb (32 MB) of NOR Flash Memory. This type of memory is able to retain data without requiring uninterrupted power. Depending on data size, writing may take up to several minutes. One typical use includes copying over a GBA ROM, thereby allowing the 3-in-1 to act like a real GBA cartridge.
  • 128 Mb (16 MB) of PSRAM. This memory does not retain data when the power is turned off. However it can be written at speeds much faster than NOR memory.
  • 4 Mb (512 KB) of battery backed SRAM for save data. The battery is necessary to retain data.
  • Programmable embedded "rumble pak" (haptic feedback) device  

 

3.기능설정 방법 

 http://blog.so-net.ne.jp/Rudolph/ 에서 「3in1_ExpPack_Tool_19a」을 클릭해서 파일을 다운 받는다. 다운로드가 불가능하면 첨부파일을 참조하도록 하자. 압축을 해제하면 롬 파일과 설명 파일이 있는데 DLDI 패치를 롬 파일에 수행한 다음 실행하면된다. 화면이 표시된 후 설정은 L, R, A, B, X, Y 버튼을 이용해서 수행할 수 있다.

 아래는 Read Me 파일에 있는 내용이다. 

PSRAM Mode:

(A) Load the selected game into PSRAM and run it.

  [select] Load the selected game into PSRAM and soft reset back to the flashcart menu.
  (only works on R4/M3 Simply, DSLink and SC DS(ONE))
 
  Until the power is turned off or the 3in1 is removed from the system, the game in PSRAM
  will be used when switching to GBA mode or using DS<->GBA linkage.

(B) Backup the save for the last game run from PSRAM.

(X) Backup all 512k of SRAM to SRAM.BIN.

(Y) Restore all 512k of SRAM from SRAM.BIN.

Games up to 16 megabytes, with SRAM, EEPROM or 512kbit Flash saves can be run from PSRAM.
Games that are larger than 16 megabytes, and games that use 1024kbit Flash saves must be
flashed to NOR.


NOR Mode:

(A) Write the selected game to NOR.

(X) Start the game in NOR.

(B) Backup the save from the NOR game to a file.

(Y) Restore the save for the NOR game from the save file.


Rumble Mode:

This screen has options for enabling rumble and the web browser RAM expansion.
The options on this screen will currently only work with the R4/M3 Simply, DSLink and the SC DS(ONE).

Select an option and press (A) to enable that feature and soft reset back to the
flashcart menu. The feature you enabled should now work for any DS game or app that
supports it.


Soft reset is executed by <START> for the R4/M3 Simply, DSLink and the SC DS(ONE) on all the Mode screens.  

 

4.마치면서... 

 NDS용 기능 확장팩인 3 in 1 Expansion Pack에 대해 간단히 알아보았다. Expansion Pack을 사용하면 GBA용 게임을 할 수 있기 때문에 게임 범위가 확장되는 장점도 있지만 홈브루 개발자라면 메인 메모리를 확장할 수 있기 때문에 램 확장에 더 관심이 있을 것이다. 

 다음에는 Expansion Pack을 설정하여 확장 램으로 바로 사용할 수 있는 방법에 대해서 알아보자. 

 

5.첨부 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

21 OS 프레임워크 소스 릴리즈

 

들어가기 전에...

 

 KKAMAGUI OS 프레임워크 설치 환경은 20 작업환경 설치 참고하면 개발 환경 및 실행 환경을 설치할 수 있다.

 

 프레임워크의 소스 코드 및 도움말은 아래와 같다.

  • FrameWork-v1.0.3.zip : KKAMAGUI OS 프레임워크 버전 1.0.3 소스 ( 2007/09/03 릴리즈 버전)

    • 화면 출력 관련 함수들을 Kernel Shell에서 StdLib로 모두 이동
    • Standard Library의 printf 형태의 kPrintf() 함수 제공으로 화면 출력의 편의성 증대
    • Kernel Shell 소스 코드 정리
    • 간단한 파일 시스템 추가
    • 00 작업일지 참고

 

  • Framework-v1.0.2.zip : KKAMAGUI OS 프레임워크 1.0.2 소스 (2007-08-31 릴리즈 버전)

    • 이클립스 환경으로 전환
    • 태스트 스위칭 및 메모리 관리 부분 포함
    • 기존의 불편한 make 방식을 수정
    • 이클립스로 컴파일 가능하도록 수정
    • djgpp 와 기타 툴체인(Cygwin, MinGW)의 충돌을 막기위해 DJGPP의 파일이름 수정
    • 자세한 내용은 20 작업환경 설치 문서 참조
    • 파일 및 링크를 위해서는 Framework 폴더에서 djmake만 입력하면 수행 가능

 

  • Framework-v1.0.1.zip : KKAMAGUI OS 프레임워크 1.0.1 소스 (2007-07-04 릴리즈 버전)

    • 커맨드 라인 방식의 빌드
    • 순수하게 프레임워크 파일만 가지고 있음
  • index-v1.0.1.zip : KKAMAGUI OS 프레임워크 설명 파일. index.chm파일 참조 (2007-07-04 릴리즈 버전)

 

2007/07/19 02:35:41 수정

 kLock(), kUnlock() 함수를 Intel CPU에서 지원하는 명령을 이용해서 새로 작성했다.

  • Asm.asm : kLock(), kUnlock() 수정

 

2007/07/11 03:45:30 수정

 make 파일을 간단하게 정리했다. makefile에 대한 사용법은 02 간단한 Make 사용법을 참조하도록 하자.

 

2007/07/10 06:23:33 수정

 큰 수정 2가지가 있었다. 각 항목은 00 작업일지를 참고하자.

  • FW.zip : asm.asm 파일과 asm.h, isr.asm 파일 수정

 

2007/07/09 20:14:58 수정

 Task.c 파일에서 kSetupTask 함수에서 버그가 발생되어 수정했다.

  • Task.c : kSetupTask() 수정

 

첨부:

287529_Framework-v1.0.3-Basic.zip
0.07MB
287537_Framework-v1.0.3-2월호.zip
0.07MB
291769_Framework-v1.0.3-3월호.zip
0.07MB
367072_Framework-v1.0.3-4월호.zip
0.08MB
146692_FrameWork-v1.0.1.zip
0.27MB
146693_index-v1.0.1.zip
1.00MB
146828_Task.c
0.00MB
148078_makefile
0.00MB
155323_Asm.asm
0.03MB
188485_FrameWork-v1.0.2.zip
0.09MB
285323_FrameWork-v1.0.3.zip
0.10MB
146828_Task.c
0.00MB
147077_FW.zip
0.01MB

20 작업환경 설치

들어가기 전에...

1.작업 환경

프레임워크의 작업환경은 아래와 같다.

2.DJGPP(GCC) 설치 및 NASM 설치

2.1 DJGPP(GCC) 설치 방법

http://www.delorie.com/djgpp/zip-picker.html 로 이동하면 필요한 항목을 선택할 수 있다.

아래와 같이 대충 설정하고 나서

맨 아래에 있는 "Tell me which files I need" 버튼을 클릭하면 아래에 보는 것과 같이 필요한 파일들이 줄줄 나온다.

unzip32.exe to unzip the zip files 95 kb

v2/copying.dj DJGPP Copyright info 3 kb
v2/djdev203.zip DJGPP Basic Development Kit 1.5 mb
v2/faq230b.zip Frequently Asked Questions 664 kb
v2/readme.1st Installation instructions 22 kb

v2gnu/bnu217b.zip Basic assembler, linker 3.9 mb
v2gnu/gcc412b.zip Basic GCC compiler 4.0 mb
v2gnu/gdb611b.zip GNU debugger 1.5 mb
v2gnu/gpp412b.zip C++ compiler 4.1 mb
v2gnu/mak3791b.zip Make (processes makefiles) 267 kb
v2gnu/txi48b.zip Info file viewer 779 kb

아래의 파일들을 다 다운 받고 그 아래쪽에 보면 설치에 관한 내용이 있는데, 그것대로 실행하면 설치는 끝난다.

C:\> mkdir djgpp  
C:\> cd djgpp  
C:\DJGPP> unzip32 d:\tmp\djdev203.zip  
C:\DJGPP> unzip32 d:\tmp\faq230b.zip  
C:\DJGPP> unzip32 d:\tmp\bnu217b.zip  
C:\DJGPP> unzip32 d:\tmp\gcc412b.zip  
C:\DJGPP> unzip32 d:\tmp\gdb611b.zip  
C:\DJGPP> unzip32 d:\tmp\gpp412b.zip  
C:\DJGPP> unzip32 d:\tmp\mak3791b.zip  
C:\DJGPP> unzip32 d:\tmp\txi48b.zip

그리고 마지막으로 "내컴퓨터" 아이콘의 속성을 클릭하여 고급 탭에 들어가서 환경 변수 설정을 해주어야 한다.

나는 "D:\djgpp" 폴더에 인스톨 했기 때문에 저렇게 입력했으니, 각자 자기 폴더에 맞도록 변경해서 입력하면 된다.

설치 확인은 cmd 창에서 gcc를 입력했을 때, 아래와 같이 나오면 정상적으로 설치가 된것이다.

C:\> gcc
gcc.exe: no input files

2.2 DJGPP 관련 파일 변경(2007/08/31)

자신의 윈도우에 다양한 툴체인이 깔려있다면, 위와 같이 PATH의 가장 앞쪽에 DJGPP 폴더를 설정하면 문제가 발생할 수 있다. 따라서 DJGPP 폴더의 bin 폴더로 이동해서 gcc.exe, gpp.exe, ld.exe, make.exe 파일 이름을 수정해 주면 어느정도 충돌을 막을 수 있다.

해당 파일들의 앞에 dj를 추가하여 djgcc.exe, djgpp.exe, djld.exe, djmake.exe로 수정하도록 하자.

2.3 NASM 설치

http://nasm.sourceforge.net/ 로 가서 파일을 다운로드 한 뒤에, DJGPP가 설치된 폴더 안의 "bin"에 압축을 풀어 실행파일을 복사해 주면 끝난다.

설치 확인은 cmd 창에서 nasm을 입력했을 때, 아무것도 안나오면 정상적으로 설치된 것이다. 만약 파일이 없다고 에러가 나면 정상적으로 복사가 되지 않은 것이니 다시 복사를 하도록 한다.

3. VirtualBox 설치

http://www.virtualbox.org/ 로 가서 파일을 다운로드 한 뒤에, Next를 눌러 설치한다. 설치가 크게 어렵지 않으므로 설치가 끝난 후, "시작" 버튼으로 가서 Virtual Box를 실행한다.

프로그램이 실행되면 "new 버튼" 을 눌러 프레임워크를 실행할 환경을 설정한다.

  • "VM Name and OS Type" 에서 Name은 "KKAMAGUI OS Framework"를 입력하고 OS Type은 "Other/Unknown"으로 입력
  • "Memory"에서 8Mbyte로 설정
  • "Virtual Hard Disk"에서 "No Hard Disk"로 설정

설정을 완료하게 되면 아래와 같은 화면이 표시된다.

여기서 Floppy를 클릭하여 아래와 같이 실행할 이미지를 설정해 준다.

그후 "Start 버튼" 을 눌러 실행을 확인한다.

4.이클립스(Eclipse) 프로젝트 생성 및 빌드

이클립스를 사용하는 경우 매우 편리하게 프로젝트를 빌드할 수 있다. 이클립스에 대한 사용방법에 대한 내용은 06 이클립스(Eclipse) CDT 설치07 이클립스(Eclipse) 단축키 및 환경설정를 참고하도록 하고 문서에 따라 OSFramework 프로젝트를 생성한 다음 해당 폴더에 프레임워크 압축파일을 풀면된다. 압축을 푼다음 프로젝트를 Refresh하거나 이클립스를 닫고 다시 실행하면 변경된 사항이 적용되어있을 것이다.

djmake.exe를 이용해서 실행해야 하므로 이클립스를 열어 Project -> Properties 를 클릭하여 아래를 djmake로 바꿔주도록 하자.

<이클립스 설정 변경>

*만약 이클립스 환경을 사용하지 않는 경우라면 기타 에디터로 소스를 수정한다음 커맨드 라인에서 DJGPP용 make.exe(djmake.exe)를 실행하면 빌드가 된다. *

5.마치면서...

이상으로 프레임워크 작업환경 설치에 대해서 알아보았다. 프레임워크가 정상적으로 컴파일 되는지 확인해 보자.

Part18. Tutorial6-간단한 파일시스템을 추가해 보자

원문 : http://kkamagui.springnote.com/pages/447164

 

들어가기 전에...

0.시작하면서...

 이번 Tutorial은 이클립스 버전으로 변경된 OS 프레임워크 1.0.3 버전 소스를 이용해서 진행하도록 하자. 이클립스 버전 프레임워크는 21 OS 프레임워크 소스 릴리즈에 있으니 다운받아서 덮어쓰면된다.

덮어 쓴 뒤에 Tutorial5의 Custom.zip 파일을 다운받아 덮어쓰면 Tutorial 6을 진행할 수 있다.

 이클립스 환경이 없는 사람은 굳이 이클립스를 깔지 않아도 되니 너무 신경쓰지 말자. 이클립스는 단순히 개발 환경을 편리하게 하는 도구일 뿐이다. 이클립스가 없더라도 커맨드 라인에서 make를 입력하여 빌드하고 테스트 할 수 있다.

 

 프레임워크 1.0.3 버전은 makefile을 대폭 수정하여 make 명령을 통해 컴파일 -> 링크 -> 이미지 파일 생성을 한번에 끝낼 수 있다. 새로운 프레임워크 소스를 설치한 사람은 DJGPPBIN 폴더에 있는 실행파일 이름을 변경해야 하는데 20 작업환경 설치를 참고하자.

 

 

1.파일 시스템 설계

 우리 주위에는 다양한 파일 시스템이 존재하며 OS에 따라 메인 파일 시스템이 존재한다. 윈도우즈의 경우는 NTFS, Linux 같은 경우는 ext3fs와 같은 파일 시스템을 사용한다. 제대로된 파일 시스템을 설계한다거나, 혹은 NTFS or FAT or ext3 와 같은 파일 시스템을 지원하는 것은 상당량의 지식과 분석이 필요하므로 간단한 파일 시스템 구현을 목표로 하자.

 위에서 언급한 파일 시스템중에 가장 간단한 파일 시스템은 FAT 파일 시스템으로 파일을 링크드 리스트(Linked List)의 형태로 관리한다. 링크를 따라가면 파일을 찾을 수가 있으며, 파일의 생성 역시 링크를 연결하는 것이 전부이다. 자료구조 자체가 아주 간단하므로 윈도우 머신부터 임베디드 시스템까지 널리 사용된다.

 FAT 파일 시스템에 대한 내용은 01 FAT 파일 시스템(File System)를 참고하도록하고 이 FAT 파일 시스템을 기반으로 간단한 파일 시스템을 만들어 보도록 하자.

 

 

1.1 파일 시스템 공간(File System Area)

 OS 프레임워크는 하드디스크 or 플로피 디스크와 같은 주변기기 I/O 함수를 거의 제공하지 않는다(적어도 지금까지는 그렇다). 그렇다면 어디에 파일 시스템을 구성할까?

 정답은 "메모리"다. 메모리에 구성된 파일 시스템을 우리는 흔히 "램 디스크(RAM Disk)"라고 부른다. 메모리에 파일 시스템을 생성하기위해서는 공간이 필요한데 동적 할당을 위한 힙으로 4Mbyte ~ 8Mbyte의 주소 공간을 이미 할당했다. 메모리 공간을 연속적으로 할당하면 램의 낭비를 줄일 수 있으므로 램디스크의 공간을 8Mbyte ~ 12Mbyte(4Mbyte의 크기)로 잡자.

 램디스크 영역을 결정했으니 Virtual Box의 램 설정을 12Mbyte 이상으로 설정하면 준비 완료다.

 

 

1.2 파일 시스템 메타 데이터(File System Meta Data) 설계

 파일 시스템 공간이 확보되었으니 파일 시스템 관리를 위한 블럭을 설정하고 할당해야 한다. 우리가 만들 간단한 시스템은 FAT 파일 시스템에서 File Allocation Table(FAT) 및 Data Area 영역만 가진 아주 간단한 구조를 가진다. File Allocation Table(FAT)의 각 엔트리(Entry)는 2Byte의 크기를 가지고 각 엔트리는 512byte 크기를 가지는 블럭을 지시한다.

 총 파일 시스템 공간의 크기가 4Mbyte이므로 이것을 512byte(한 섹터의 크기)로 나누면 8192개의 엔트리가 존재한다. 하나의 엔트리당 2Byte 공간을 차지하므로 FAT 공간은 총 16384Byte(16Kbyte)의 크기이다.

 위의 내용을 그림으로 표현하면 아래와 같다.

filesystem1.PNG

<File System Architecture>

 

8Mbyte ~ 8Mbyte + 16Kbyte는 FAT 엔트리를 위한 영역이고 8Mbyte + 16Kbyte ~ 12Mbyte까지는 데이터를 위한 영역이다. 위 그림의 화살표가 의마하는 것은 FAT의 엔트리와 Data Area의 한 블럭이 1:1 대응임을 의미한다. FAT 파일 시스템과 동일한 방식이므로 FAT 파일 시스템의 용어를 사용하여 Data Area의 한 섹터(Sector)를 클러스터(Cluster)라고 부르자.

 

 

1.3 디렉토리 및 파일 설계

 파일 시스템에는 루트 디렉토리(Root Directory)라는 것이 존재한다. 이것은 트리(Tree) 형태를 가지는 디렉토리 구조에서 트리의 루트(Root)에 해당하는 것으로 모든 파일 및 폴더의 시작점이다. 우리가 파일에 접근할때 OS로 넘겨주는 절대 경로를 자세히 들여다 보면 루트 디렉토리부터 시작하는 경로를 넘겨준다("C:\ABC\a.txt" or "/home/kkamagui/a.txt"  같은 경로가 절대 경로이다). 저 경로를 받아서 OS에서는 루트 디렉토리부터 차례로 접근하여 파일 또는 폴더를 찾는 것이다.

 

 그렇다면 디렉토리 구조에서 디렉토리와 파일은 어떻게 다르며 어떻게 구분할까?

 일반적으로 섹터내에 존재하는 데이터라는 관점에서 디렉토리도 파일과 동일하다. 다른점은 파일은 저장한 데이터를 가지지만, 디렉토리는 다른 디렉토리 또는 파일에 대한 정보를 가진다는 점이다.

 

 위에서 디렉토리와 파일에 대해서 차이점을 간략하게 알아봤으니 디렉토리 구조를 디자인 하자. 디렉토리 또한 파일의 형태이므로 클러스터 단위로 구성되게 되는데, 클러스터 단위(섹터 크기와 동일 = 512Byte)를 엔트리 크기로 나누어서 나머지가 0이되게하면 클러스터 단위로 디렉토리를 처리할 수 있으니 구현이 간단해 진다.

 디렉토리 구조의 경우, 최소한의 데이터를 포함해야 하는데, 그중 하나는 엔트리의 이름이고 나머지 하나는 엔트리의 데이터가 시작되는 클러스터 인덱스다. 클러스터 인덱스의 정보는 FAT 내의 엔트리 인덱스와 일치하므로 클러스터 번호를 이용해서 FAT 내의 엔트리에 접근할 수 있다. 그렇다면 FAT 내의 엔트리가 2Byte로 구성되어있으므로 디렉토리 엔트리 역시 클러스터 인덱스가 2Byte로 구성되어야 하고, 이것을 바탕으로 디렉토리 엔트리는 아래와 같이 구성할 수 있다.

  • 파일 이름 : 11Byte 
  • 디렉토리/파일 속성 플래그 : 1Byte 
  • FAT 엔트리 인덱스 : 2Byte
  • FILE 크기 : 2Byte

 위와 같이 구성하면 총 16Byte가 필요하다. 512Byte를 16Byte으로 나누면 총 32개의 엔트리로 딱 떨어지므로 적절한 선택인것 같다.

 만약 디렉토리의 엔트리가 32개가 꽉차서 더이상 넣지 못하는 경우라면 어떻게 할까? 새로운 클러스터를 할당받아서 디렉토리 클러스터에 연결하고 새로 할당받은 클러스터에 엔트리를 생성하면 된다. 클러스터를 연결하는 부분은 각자 구현해 보도록하고 지금은 구현을 간단히 하기위해 32개로 제한하자.

 

 

1.4 파일 시스템 API

 우리가 설계한 파일 시스템은 FAT와 Data Area간의 연결을 통해 파일 및 디렉토리를 관리하므로 이를 관리하고 사용할 API가 필요하다. Standard C API 수준으로까지 파일 시스템 API를 제공하려면 많은 부분을 고민해야하니 Low Level 수준의 API만 제공하고 추후 업그레이드를 하는 걸로 하자.

 Low Level 수준의 API란 어떤 것일까? 아래와 같은 함수들이 Low Level 함수라고 보면 된다(간략하게 표현했다).

  • 섹터 할당 및 FAT 관련 함수 
    • Alloc Cluster() : 빈 클러스터를 하나 할당하여 Alloc으로 마크하고 그 인덱스를 반환
    • Free Cluster( a ) : 넘겨 받은 인덱스 a 클러스터를 Free로 마크
    • Link Cluster( a, b ) : a 클러스터의 뒤에 b 클러스터를 링크 
    • Unlink Cluster( a ) : a 클러스터 뒤에 연결된 링크를 끊음
    • Get Next Cluster( a ) : a 클러스터 뒤에 연결된 다음 클러스터의 인덱스를 반환

 

  • 디렉토리 엔트리 관련 함수 
    • Make Entry( directory cluster, name, attribute, file cluster ) : directory sector 인덱스가 가리키는 클러스터의 내용에 name의 이름을 가지고 attribute의 속성을 가지면서 file sector 클러스터를 시작으로하는 엔트리를 생성
    • Delete Entry( directory cluster, name ) : directory cluster 인덱스가 가리키는 클러스터의 내용을 조사하여 name을 가지는 엔트리를 삭제

 

  • I/O 관련 
    • Read Sector( sector, buffer ) : sector 인덱스의 데이터를 읽어서 buffer에 복사 
    • Write Sector( sector, buffer ) : buffer의 값을 sector 인덱스의 데이터로 복사
    • Read Cluster( cluster, buffer ) : cluster 인덱스의 데이터를 읽어서 buffer에 복사 
    • Write Cluster( cluster, buffer ) : buffer의 값을 cluster 인덱스의 데이터로 복사

  섹터 인덱스는 0x800000(8Mbyte)를 시작으로 하여 섹터 단위(512Byte) 단위로 전체를 나눈 인덱스를 의미한다. 즉 0 섹터의 시작 주소는 0x800000(8Mbyte) + 512 * 0 이 되고, 1 섹터는 0x800000(8Mbyte) + 512 * 1이 되는 것이다.

 반면에 클러스터의 주소는 Data Area의 다음에 위치하는 공간을 512Byte로 나눈 인덱스를 의미한다. 즉 FAT 영역이 16Kbyte를 차지하므로 0번 클러스터의 시작 주소는 0x800000(8Mbyte) + 16Kbyte + 512 * 0 이 되고 1번 클러스터의 시작 주소는 0x800000(8Mbyte) + 16Kbyte + 512 * 1이 된다.

 

1.5 파일 시스템 기능 요약

  • 루트 디렉토리(Root Directory)로 부터 트리(Tree) 형태로 이루어지는 디렉토리 구조 지원
  • 한 디렉토리 당 생성 가능한 디렉토리 및 파일의 개수는 총 32개
  • 파일은 클러스터 링크를 연결함에 따라 가변적 크기 가능

 

 

2.구현

 파일 시스템이나 메모리 관리 같이 세세한 디버깅이 필요한 부분은 커널을 실행하여 테스트하기가 까다롭다. 특히나 Step By Step 형식으로 접근하기 힘든 커널 디버깅은 복잡한 부분에 대한 분석이 더욱 힘들다. 따라서 알고리즘이 결정되면 Visual C++과 같은 개발툴로 알고리즘을 검증하여 정상 동작함을 확인하고 이것을 포팅하는 것이 효율적이다. 실제로 프레임워크의 동적 메모리 할당 코드와 파일 시스템 코드를 Visual C++로 코딩하여 각 케이스를 모두 테스트한 뒤 포팅하였다.

 

2.1 Visual C++ 코드

 Visual C++과 프레임워크의 차이라면 같은 역할을 하는 함수의 이름(memset -> kMemSet, memcpy -> kMemCpy 등등)이 다른 점과 주소공간(프레임워크는 8M~12M의 공간 사용, Visual C++에서는 임의의 할당된 메모리 공간 사용)이 다른 점 정도다. 이 정도의 차이는 매크로를 정의하면 쉽게 포팅할 수 있다. 자세한 부분은 첨부에 포함된 Visual C++용 프로젝트 파일을 참고하자.

FileSystem1(1).PNG

<Visual C++에서 파일 시스템 코드를 실행한 화면>

 

2.2 프레임워크 코드

2.2.1 FileSystem.h 수정

 아래는 위에서 언급한 기능들을 정의해 놓은 FileSystem.h 파일이다.

  1. /**
        File System Management
            파일 시스템에 대한 처리
  2.     Written KKAMAGUI, http://kkamagui.egloos.com
    */
    #ifndef __FILESYSTEM_H__
    #define __FILESYSTEM_H__
  3. //#include <windows.h>
    #include "../FW/DefineMacro.h"
  4. // 클러스터와 섹터의 크기 정의
    #define SECTORSIZE  512
    #define CLUSTERSIZE SECTORSIZE
  5. // 램디스크을 위한 메모리 주소의 시작과 끝
    #define RAMDISK_START_ADDRESS   ( 8 * 1024 * 1024 )
    #define RAMDISK_END_ADDRESS     ( 12 * 1024 * 1024 )
  6. // 램디스크 크기 및 기타 데이터
    #define RAMDISK_SIZE            ( RAMDISK_END_ADDRESS - RAMDISK_START_ADDRESS )
    #define RAMDISK_DATA_START_ADDRESS  ( RAMDISK_SIZE / SECTORSIZE * 2 )
    #define RAMDISK_FATENTRYCOUNT       ( RAMDISK_SIZE / SECTORSIZE )
  7. // 디렉토리 엔트리의 속성 정의
    #define DIRECTORY_ENTRY_FILE        0x01
    #define DIRECTORY_ENTRY_DIRECTORY   0x02
  8. // 디렉토리 엔트리 구조체
    typedef struct directoryEntry
    {
        char vcName[ 11 ];
        BYTE bAttribute;
        WORD wClusterIndex;
        WORD wSize;
    } DIRECTORYENTRY, * PDIRECTORYENTRY;

  9. // 함수 목록
    void InitFileSystem( void );
    BOOL MakeFile( char* pcAbsPath, BYTE bAttribute );
    BOOL DeleteFile( char* pcAbsPath );
    BOOL FindDirectoryClusterFromAbsPath( char* pcAbsPath, int* piClusterIndex );
    BOOL ReadCluster( int iClusterIndex, BYTE* pbBuffer );
    BOOL WriteCluster( int iClusterIndex, BYTE* pbBuffer );
  10. #endif /*FILESYSTEM_H_*/

  파일 및 디렉토리를 생성하고 삭제하는 기능과 클러스터를 읽고 쓰는 기능을 지원한다. 클러스터에 링크를 연결하는 기능이 구현되어있으나 파일 생성 및 클러스터 링크 기능은 지금 사용하지 않으므로 헤더 파일에 포함시키지 않았다. 현재는 파일 및 디렉토리 엔트리를 디렉토리 구조에 생성하고 삭제하는 기능만 포함되어있으니 추후 필요하면 확장하도록 하자.

 FileSyste.c 파일은 내용이 많으므로 아래 첨부에서 소스를 다운받아 보도록 하자. 위에서 언급한 설계대로 구현 했기 때문에 문서와 같이 보면 크게 어렵지 않을 것이다.

 

2.2.2 KShell.c/h 수정

 디렉토리를 생성하는 명령 mkdir과 디렉토리를 삭제하는 명령 rmdir, 그리고 디렉토리를 표시하는 명령 ls를 ProcessCommand() 함수에 추가하였다.

  1.  /**
        엔터가 쳐졌으면 버퍼의 내용으로 명령어를 처리한다.
    */
    void ProcessCommand(char* vcCommandBuffer, int* piBufferIndex)
    {
        char vcDwordBuffer[ 8 ];
        static DWORD vdwValue[ 10 ];
        static int iCount = 0;
  2. ...... 생략 ......
  3.     // Directory를 출력한다.
        else if( ( *piBufferIndex > 3 ) &&
                 ( kMemCmp( vcCommandBuffer, "ls", 2 ) == 0 ) )
        {
            vcCommandBuffer[ *piBufferIndex ] = '\0';
            PrintDirectory( vcCommandBuffer + 3 );
        }
        // Directory를 만든다.
        else if( ( *piBufferIndex > 6 ) &&
                 ( kMemCmp( vcCommandBuffer, "mkdir", 5 ) == 0 ) )
        {
            vcCommandBuffer[ *piBufferIndex ] = '\0';
            MakeFile( vcCommandBuffer + 6, DIRECTORY_ENTRY_DIRECTORY );
        }
        // Directory를  지운다.
        else if( ( *piBufferIndex > 6 ) &&
                 ( kMemCmp( vcCommandBuffer, "rmdir", 5 ) == 0 ) )
        {
            vcCommandBuffer[ *piBufferIndex ] = '\0';
            DeleteFile( vcCommandBuffer + 6 );
        }   
    }
  4. ...... 생략 ......
  5. /**
        디렉토리를 출력한다.
    */
    void PrintDirectory( char* pcPath )
    {
        int iClusterIndex;
        BYTE vbCluster[ CLUSTERSIZE ];
        DIRECTORYENTRY* pstEntry;
        int i;
        char vcBuffer[ 8 ];
  6.     kPrintf( "==== Directory = [%s] ====\n", pcPath );
       
        // Path로 Cluster를 찾는다.
        if( FindDirectoryClusterFromAbsPath( pcPath, &iClusterIndex ) == FALSE )
        {
            kPrintf( "Error!!! Can't Find Cluster\n" );
            return ;
        }
  7.     // Cluster를 읽는다.
        if( ReadCluster( iClusterIndex, vbCluster ) == FALSE )
        {
            return ;
        }
  8.     // 클러스터를 모두 돌면서 Entry를 뽑는다.
        pstEntry = ( DIRECTORYENTRY* ) vbCluster;
        for( i = 0 ; i < ( CLUSTERSIZE / sizeof( DIRECTORYENTRY ) ) ; i++ )
        {
            if( pstEntry->vcName[ 0 ] == '\0' )
            {
                pstEntry++;
                continue;
            }
           
            kPrintf( "%s     ", pstEntry->vcName );
  9.         if( pstEntry->bAttribute == DIRECTORY_ENTRY_FILE )
            {
                kPrintf( "%s     ", "FILE" );
            }
            else
            {
                kPrintf( "%s     ", "DIRECTORY" );
            }
  10.         kPrintf( "%X    %X\n", pstEntry->wSize, pstEntry->wClusterIndex ); 
            pstEntry++;
        }
        kPrintf( "\n" );
    }

 

 

3.Simple File System의 효용성

 앞서 설명했듯이 이 파일 시스템은 작은 용량의 램을 할당받아 저장매체로 사용하는 램디스크이다. 램디스크의 특성상 재부팅하면 내용이 사라진다. 저장매체라면 당연히 그 정보가 남아있어야 하는데, 그러한 관점에서 보면 이 램디스크는 쓸모 없는 것이 아닌가?

 물론 데이터가 유지되지 않는다는 점에서 크게 마이너스지만... 램 디스크의 특성이 그러하듯 매우 빠른 검색과 파일 생성/디렉토리 생성이 가능하다. 그렇기에 새로운 파일 시스템의 테스트용으로 아주 적절하다(같은 수의 파일을 생성했다가 지운다고 가정할때 하드디스크가 빠르겠는가? 램디스크가 빠르겠는가?)

 재부팅 후에 내용이 사라지는 문제도 쉽게 해결할 수 있다. 주기적으로 램디스크의 내용을 하드디스크에 덤프하여 보관하고 OS가 부팅되었을때 하드디스크의 내용을 메모리로 로드하면 된다. 추후 램디스크의 내용이 보관되어야 하거나 지속적으로 관리되어야 하는 경우 위의 방법으로 해결하면 된다. 혹시나 파일 시스템이 너무 간단하여 쓸일이 있을까 하는 사람들을 위해 노파심에 끄적여 보았다. 기능이 부족하다고 생각되면 필요에 맞게 수정하면되니 너무 걱정하지 말자.

 

4.마치면서...

 이상으로 간단한 파일 시스템을 구현해 보았다.

 

5.첨부

5.1 프레임워크 1.0.3 이전 버전

 

5.2 프레임워크 1.0.3 이후 버전

 

 

 

이 글은 스프링노트에서 작성되었습니다.

Part17. Tutorial5-메모리 동적할당 기능에 동기화 기능을 추가해 보자

원문 :  http://kkamagui.springnote.com/pages/399536

 

들어가기 전에...

0.시작하면서...

 앞서 이미 동적할당 기능을 버디 블럭(Buddy Block) 알고리즘으로 구현했다. 하지만 아직 멀티 태스킹 환경에서 안전하게 동작하기 위해서는 무엇인가 부족하다. 멀티 태스크 환경에서 실행했던 화면에 문자를 찍는 예제처럼, 메모리 영역이라는 자원을 가지고 서로 경쟁하는 상황이다.

 이 문제를 동기화 객체중에 하나인 바이너리 세머포어(Binary Semaphore)를 이용해서 해결해 보자.

 

1.보호 or 임계 영역(Critical Section)의 결정

 동적 메모리 할당 함수에서 경쟁관계가 발생하는 범위는 어디서 어디까지일까? 어디까지를 보호하면 될까?

 보호는 공통으로 사용하는 자원에 대해서 범위를 결정해야 한다. 동적 메모리 할당에서 공통으로 사용하는 구조체는 gs_stMemory이고 따라서 구조체를 변경하는 부분은 다 보호해야 한다.

 보호하는 영역의 크기와 성능간에는 멀티 태스킹 환경일때 밀접한 관계가 있는데, 보호하는 영역이 넓으면 코드의 로직이 간단해 지는 반면 병렬 수행 능력이 떨어지므로 전체적인 성능이 저하된다. 보호하는 영역은 좁을수록 좋으니 신중이 결정하도록 하자.

 

2.AllocMemory() 함수 분석

 AllocMemory() 함수를 직접 보면서 보호할 범위를 찾아보자.

  1.  /**
        메모리를 할당한다.
    */
    void* AllocMemory( int iSize )
    {
        int iAlignSize;
        int iOffset;
        int iRelAddress;
        int iSizeArrayOffset;
  2.     // 크기를 못구하면 에러다.
        iAlignSize = GetAlignSize( iSize );
        if( iAlignSize == 0 )
        {
            DEBUGPRINT( "HERE" );
            return NULL;
        }
  3.     // 메모리 블럭 하나를 얻는다.
        iOffset = AllocBuddy( iAlignSize );
        if( iOffset == -1 )
        {
            return NULL;
        }
       
        // START Address에서의 상대적인 위치를 계산하여 Size Array에 크기를 넣어둔다.
        // 나중에 Free할때 사용하기 위해서다.
        iRelAddress = iAlignSize * iOffset;
        iSizeArrayOffset = iRelAddress / MEMORY_ALLOC_MIN_SIZE;
        gs_stMemory.vdwSizeArray[ iSizeArrayOffset ] = iAlignSize;
  4.     return ( void* ) ( iRelAddress + MEMORY_START_ADDRESS );
    }

 

 중간에 보면 AllocBuddy() 함수를 호출하고 그 값을 받아서 gs_stMemory의 값을 수정하는 코드를 볼 수 있다. 동기화를 가장 쉽게 하려면 위의 AllocMemory() 함수 전체를 Lock/Unlock 으로 보호하면 된다. 하지만 효율적이지 못한 방법임을 우리는 잘 알고 있다(아닌가? 나만 알고있나? ㅡ,.ㅡ;;;).

 좀 더 생각하보면 AllocBuddy()에서 값을 할당받은 후 아래는 그 값으로 계산하여 설정하는 것이기 때문에 AllocBuddy() 함수만 보호하면 될 것 같다는 생각이 든다.

 음... 그럼 조금만 더 생각해 보자. AllocBuddy() 함수에서 실제로 gs_stMemory에 접근하는 부분을 찾아 세분화하면 더 좁게 만들 수 있다.

  1. /**
        버디 블록 알고리즘으로 메모리를 할당한다.   
            정렬된 크기가 들어와야 한다.
    */
    int AllocBuddy( int iAlignSize )
    {
        int iListIndex;
        int iFreeOffset;
  2.     iListIndex = GetListIndexOfMatchSize( iAlignSize );
        if( iListIndex == -1 )
        {
            return -1;
        }
  3.     // 해당 Index에 Bitmask를 검색해서 Free한 곳이 있는가 본다.
        iFreeOffset = FindFreeOffsetInMask( iListIndex );
        // 만약 최대크기의 listIndex인데도 없으면 실패한다.
        if( ( iFreeOffset == -1 ) && ( iListIndex == MEMORY_MAX_BITMASKLISTCOUNT -1 ) )
        {
            return -1;
        }
        else if( iFreeOffset != -1 )
        {
            SetFlagInMask( iListIndex, iFreeOffset, MEMORY_ALLOC );
            return iFreeOffset;
        }

  4.     
        // 여기까지 오면 해당 Bitmask에 빈곳이 없고 최대크기의 bitmask도 아니라는
        // 이야기이므로 2배 크기에서 할당가능한가 확인한다.
        iFreeOffset = AllocBuddy( iAlignSize * 2 );
        if( iFreeOffset == -1 )
        {
            return -1;
        }
  5.     // Free Offset이 할당되었으면 2배 크기를 요청했으므로 하나는 리턴하고
        // 다른 하나는 bitmask에 연결해야 한다.
        // 2 * iFreeOffset을 하면 하위의 첫번째 빈 블럭을 찾을 수 있다.
        // 첫번째 블럭은 할당하고 두번째 블럭은 Free 상태로 둔다.
        SetFlagInMask( iListIndex, 2 * iFreeOffset, MEMORY_ALLOC );
        SetFlagInMask( iListIndex, ( 2 * iFreeOffset ) + 1, MEMORY_FREE );

  6.     
        return ( iFreeOffset * 2 );
    }

 

 위의 코드에서 FindFreeOffsetInMask() 함수를 이용해서 빈 블럭을 얻어오는 부분과 SetFlagInMask() 함수 이용해서 마스크를 설정하는 부분만 손 보면 될 것 같다. 저 두 부분에 Lock/Unlock을 추가 하도록 하자.

 

3.FreeMemory() 함수 분석

 FreeMemory() 함수도 직접 보면서 찾아보도록 하자.

  1. /**
        메모리를 해제한다.
    */
    BOOL FreeMemory( void* pvAddress )
    {
        int iRelAddress;
        int iSizeArrayOffset;
        int iBlockSize;
        int iListIndex;
        int iMaskOffset;
  2.     if( pvAddress == NULL )
        {
            return FALSE;
        }
  3.     // 실제 주소에서 상대적인 주소로 변경하여 할당했던 블럭의 크기를 알아온다.
        iRelAddress = ( ( DWORD ) pvAddress ) - MEMORY_START_ADDRESS;
        iSizeArrayOffset = iRelAddress / MEMORY_ALLOC_MIN_SIZE;
  4.     iBlockSize = gs_stMemory.vdwSizeArray[ iSizeArrayOffset ];
        gs_stMemory.vdwSizeArray[ iSizeArrayOffset ] = 0;
  5.     iListIndex = GetListIndexOfMatchSize( iBlockSize );
        if( IsValidListIndex( iListIndex ) == FALSE )
        {
            return FALSE;
        }
  6.     iMaskOffset = ( iRelAddress / iBlockSize );
  7.     return FreeBuddy( iListIndex, iMaskOffset );
    }

 

 FreeBuddy() 라는 함수가 보인다.  AllocMemory() 함수의 경우와 마찬가지로 좀더 세분화 해보자. 아래는 FreeBuddy() 함수이다.

  1. /**
        List Index의 Mask Offset 위치의 Block을 Free 한다.
    */
    BOOL FreeBuddy( int iListIndex, int iMaskOffset )
    {
        BOOL bFlag;
        int iBuddyOffset;
  2.     // 해당 블럭을 Free한 상태로 만든다.
        SetFlagInMask( iListIndex, iMaskOffset, MEMORY_FREE );
  3.     // 최대 크기 블럭이면 여기서 끝
        if( iListIndex == MEMORY_MAX_BITMASKLISTCOUNT -1 )
        {
            return TRUE;
        }
  4.     // Mask offset이 짝수이면 다음 블럭을 보고 홀수이면 이전블럭을 봐서 Free상태
        // 이면 합쳐서 상위 하나의 블럭을 만들 수 있으므로 상태를 살펴본다.
        if( iMaskOffset % 2 == 0 )
        {
            iBuddyOffset = iMaskOffset + 1;
        }
        else
        {
            iBuddyOffset = iMaskOffset - 1;
        }


  5.   // 해당 Mask의 Flag 값을 얻는다.
        if( GetFlagInMask( iListIndex, iBuddyOffset, &bFlag ) == FALSE )
        {
            return FALSE;
        }
       
        // 이웃이 할당된 상태이면 여기서 끝
        if( bFlag == MEMORY_ALLOC )
        {
            return TRUE;
        }
  6.     // 이웃이 빈 상태이면 둘다 합쳐서 상위 블럭을 만들어 준다.
        SetFlagInMask( iListIndex, iMaskOffset, MEMORY_ALLOC );
        SetFlagInMask( iListIndex, iBuddyOffset, MEMORY_ALLOC );
       
        return FreeBuddy( iListIndex + 1, iMaskOffset / 2 );
    }

 

 Free의 경우는 Alloc의 경우와 약간 다르다. Free의 경우는 Free한 직 후(위의 붉은색 영역) 해당 블럭의 마스크를 변경한 다음 이웃한 블럭을 보고 이웃한 블럭이 Free 된 상태이면 하나로 합하여 위로 올리는 과정을 거친다.

 블럭을 합치는 동안에 Free된 블럭에 대한 할당 요청이 들어오면 어떻게 될까? 그 이후의 블럭을 합치는 과정은 하위 블럭들이 전부 Free 상태라는 가정에서 동작하므로 문제가 발생한다. 따라서 Free 같은 경우는 블럭을 합치는 전체 과정이 보호(함수 전체에 대한 보호)되어야 한다.

 

3.구현

3.1 Memory.c/h 수정

 동적 메모리 할당 및 해제를 위한 동기화 오브젝트를 하나 만들자. 그리고 메모리 초기화를 할때 같이 초기화 하자.

  1. MEMORY gs_stMemory;
    SEMAPHORE gs_stSema;
  2. /**
        메모리를 초기화 한다.
    */
    void InitMemory( void )
    {
        // 동기화 오브젝트 초기화
        InitSemaphore( &gs_stSema, 1 );
     
        // 모두 할당 안된 것으로 설정한다.
        kMemSet( &gs_stMemory, 0x00, sizeof( gs_stMemory ) );
       
        // 제일 큰 블럭 하나만 Free한 상태이다.
        gs_stMemory.vvbBitMask[ MEMORY_MAX_BITMASKLISTCOUNT - 1 ][ 0 ] = MEMORY_FREE;
    }

 

 앞서 구분했던 영역에 동기화 코드를 삽입한다.

  1. /**
        버디 블록 알고리즘으로 메모리를 할당한다.   
            정렬된 크기가 들어와야 한다.
    */
    int AllocBuddy( int iAlignSize )
    {
        int iListIndex;
        int iFreeOffset;
  2.     iListIndex = GetListIndexOfMatchSize( iAlignSize );
        if( iListIndex == -1 )
        {
            return -1;
        }
       
        OnSemaphore( &gs_stSema );
        // 해당 Index에 Bitmask를 검색해서 Free한 곳이 있는가 본다.
        iFreeOffset = FindFreeOffsetInMask( iListIndex );
        // 만약 최대크기의 listIndex인데도 없으면 실패한다.
        if( ( iFreeOffset == -1 ) && ( iListIndex == MEMORY_MAX_BITMASKLISTCOUNT -1 ) )
        {
            OffSemaphore( &gs_stSema );
            return -1;
        }
        else if( iFreeOffset != -1 )
        {
            SetFlagInMask( iListIndex, iFreeOffset, MEMORY_ALLOC );
            OffSemaphore( &gs_stSema );
            return iFreeOffset;
        }
        OffSemaphore( &gs_stSema );
        
        // 여기까지 오면 해당 Bitmask에 빈곳이 없고 최대크기의 bitmask도 아니라는
        // 이야기이므로 2배 크기에서 할당가능한가 확인한다.
        iFreeOffset = AllocBuddy( iAlignSize * 2 );
        if( iFreeOffset == -1 )
        {
            return -1;
        }
       
        OnSemaphore( &gs_stSema );
        // Free Offset이 할당되었으면 2배 크기를 요청했으므로 하나는 리턴하고
        // 다른 하나는 bitmask에 연결해야 한다.
        // 2 * iFreeOffset을 하면 하위의 첫번째 빈 블럭을 찾을 수 있다.
        // 첫번째 블럭은 할당하고 두번째 블럭은 Free 상태로 둔다.
        SetFlagInMask( iListIndex, 2 * iFreeOffset, MEMORY_ALLOC );
        SetFlagInMask( iListIndex, ( 2 * iFreeOffset ) + 1, MEMORY_FREE );
        OffSemaphore( &gs_stSema );
        
        return ( iFreeOffset * 2 );
    }

 

  1.  /**
        메모리를 해제한다.
    */
    BOOL FreeMemory( void* pvAddress )
    {
        int iRelAddress;
        int iSizeArrayOffset;
        int iBlockSize;
        int iListIndex;
        int iMaskOffset;
        BOOL bRet;
  2.     if( pvAddress == NULL )
        {
            return FALSE;
        }
  3.     // 실제 주소에서 상대적인 주소로 변경하여 할당했던 블럭의 크기를 알아온다.
        iRelAddress = ( ( DWORD ) pvAddress ) - MEMORY_START_ADDRESS;
        iSizeArrayOffset = iRelAddress / MEMORY_ALLOC_MIN_SIZE;
  4.     // 만약에 할당되어있지 않으면 Free하면 안된다.
  5.     // 버그가 있어서 수정한 부분이다.
  6.     if( gs_stMemory.vdwSizeArray[ iSizeArrayOffset ] == 0 )
        {
            return FALSE;
        }
  7.     iBlockSize = gs_stMemory.vdwSizeArray[ iSizeArrayOffset ];
        gs_stMemory.vdwSizeArray[ iSizeArrayOffset ] = 0;
  8.     iListIndex = GetListIndexOfMatchSize( iBlockSize );
        if( IsValidListIndex( iListIndex ) == FALSE )
        {
            return FALSE;
        }
  9.     iMaskOffset = ( iRelAddress / iBlockSize );
  10.     OnSemaphore( &gs_stSema );
        bRet = FreeBuddy( iListIndex, iMaskOffset );
        OffSemaphore( &gs_stSema );
       
        return bRet;
    }

 

3.2 KShell.c 수정

 이제 멀티 태스킹 환경에서 동작을 시험해 볼 차례이다. KShell.c 파일을 열어서 수정한다.

  1. /**
        테두리를 그려주는 태스크
    */
    void EdgeDraw( void )
    {
        int i;
        int j;
        BYTE bCh;
        int k;
        char vcBuffer[ 8 ];
        int iTID;
        DWORD vdwAllocAddress[ 2 ];
  2.     i = 0;
        j = 0;
        bCh = 0;
        iTID = GetCurrentTID();
        
        kDToA( vcBuffer, iTID );
  3.     for( k = 0 ; k < 50000 ; k++ )
        {
            printxy( 0, 23 - iTID, "=EdgeDraw Task Work=" );
            printxyn( 20, 23 - iTID, vcBuffer, 8 );
  4.         // 콘솔 테두리를 돌면서 .을 찍는다.
            for( i = 30 ; i < 79 ; i++ )
            {
                // Memory Alloc
                vdwAllocAddress[ 0 ] = ( DWORD ) AllocMemory( kRand() % 0x400000 );
                vdwAllocAddress[ 1 ] = ( DWORD ) AllocMemory( kRand() % 0x400000 );
  5.             // Semaphore 대기
                OnSemaphore( &gs_stSema );
                gs_iX = i;
                gs_iY = 23 - iTID;
                Print( bCh );
                bCh++;
                OffSemaphore( &gs_stSema );
                SwitchTask();
  6.             FreeMemory( ( void * ) vdwAllocAddress[ 0 ] );
                FreeMemory( ( void * ) vdwAllocAddress[ 1 ] );
            }
        }
    }

 

4.실행

 이제 태스크를 실행하면 2개의 메모리를 랜덤한 크기로 할당받은 후, 화면에 문자를 출력하고 다시 해제하는 것을 반복할 것이다.

 위의 수정이 다 끝나면 kernelclean.batmakekernel.bat, makeimg.bat 를 차례로 실행시켜서 disk.img 파일을 만든 다음 이것을 실행해 보자. 멀티 태스킹 환경에서 잘 동작하는지 확인하기 위해 커널이 실행되면 starttask 명령을 입력하여 태스크를 여러개 실행한 다음 dumpmem 명령으로 비트마스크를 확인하면 된다. 랜덤하게 메모리가 할당되므로 dumpmem 수행 시 마다 비트맵이 다르게 나옴을 알 수 있다.

multimem1.PNG

<태스크를 여러개 수행시킨 상태>

 

multimem2.PNG

<비트 마스크 덤프1>

 

 multimem3.PNG

<비트 마스크 덤프2>

 

 

5.마치면서...

 이것으로 멀티 태스킹 환경에서 malloc/free를 마음껏 할 수 있게 되었다. 다시 커널 프로그래밍에 빠져보자 >ㅁ<)/~

 

6.첨부

6.1 프레임워크 1.0.3 이전 버전

 

6.2 프레임워크 1.0.3 이후 버전

 

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

Part16. Tutorial4-메모리 동적할당(malloc, free) 기능을 추가해 보자

원문 :  http://kkamagui.springnote.com/pages/391653

 

들어가기 전에...

 

 

0.시작하면서...

 이번에는 프로그래밍에서 빠져서는 안될 부분, 메모리 동적 할당(malloc, free)을 한번 구현해 보자. 이미 동기화 오브젝트에 대한 구현이 끝났기 때문에 멀티 태스킹 환경에서도 잘 동작하는 동적 할당 모듈을 작성할 수 있다. 오늘은 간단히 동적할당에 대해서만 구현하고 동기화는 다음에 따로 구현해보자.

 

1.메모리 공간 할당

 동적할당을 구현하기 위해서는 동적 할당을 위한 공간을 마련해야 하는데, 이전에 봤던 OS 프레임워크의 메모리 레이아웃을 다시 보자(너무 많이봐서 외우는 사람도 있을 것이다 ㅎㅎ).

메모리_레이아웃.PNG

<프레임워크 메모리 레이아웃>

 

 커널 스택의 끝이 0x400000에 위치하므로 4Mbyte 이상의 영역은 비어있다. 이 공간을 동적 할당의 공간으로 사용하자. 그럼 시작 주소는 4M로 정해졌고 이제 영역의 끝을 정해야하는데, 끝 주소를 8M 정도로 하면 괜찮을 듯 하다. 작은 커널이기 때문에 메모리 사용이 그렇게 많지 않을 것이고, 혹여 메모리를 많이 쓴다면 끝 주소를 더 늘리면 되므로 걱정하지 말자. 단 Virtual Box의 메모리도 거기에 맞추어 변경해줘야 하는데 지금은 8Mbyte 이상으로 설정하면 충분하다.

 

2.메모리 할당 정책

 자 이제는 어떤 알고리즘으로 메모리를 할당할까?

 

 생각할 수 있는 제일 간단한 방법은 무조건 고정크기의 블럭을 할당하는 것이다. 즉 프로그램은 크기를 요청할 수 없고, 메모리 할당을 요청하면 특정 크기의 블럭을 할당 받기 때문에 "알아서" 사용해야 한다. 블럭의 크기를 관리할 필요가 전혀 없기때문에 아주 간단하다. 천지 쓸모 없을 것 같은 방법이지만... 실제로 이와 비슷하게 할당 받아서 "잘" 동작하는 어플리케이션이 있다.

 대용량 데이터를 처리하는 데이터베이스와 같은 프로그램의 경우, 운영체제로부터 큰 블럭을 할당받고 나름 최적화된 알고리즘으로 나누어 사용한다. 어플리케이션만 똑똑하다면 얼마든지 가능하다는 이야기다. 단점은 어플리케이션 개발자를 혹사 시킬 수도 있다는... ㅡ_ㅡ;;;( 요즘 메모리 관리를 못하는 어설픈 개발자가 어디 한둘이어야... ㅡ_ㅡ;;;;; )

 

 두번째 방법은 블럭 영역을 검색해서 할당하는 방법이다. 제일 딱맞는 크기를 할당해 준다던지, 아니면 첫번째 만난 가능한 공간을 할당해 준다던지 하는 알고리즘으로 할당 가능한 영역을 찾아서 할당해 주는데, Best fit이니 Worst fit 이니 하는 방법들로 널리 알려져있다. 이 방법을 사용하면 가변 크기의 블럭들을 할당하고 해제할때 발생하는 단편화(Fragmentation)을 피하기가 어렵다.

 

 세번째 방법은 좀 더 스마트(Smart)한 방법인데, 익히 버디 블럭(Buddy Block) 알고리즘으로 알려진 방법이다(Linux에서 채택하고 있다). 이름 그대로 친한 친구끼리 어떻게 저떻게 처리하는 알고리즘인데, 아래에서 자세히 알아보자.

 

3.버디 블럭 알고리즘(Buddy Block Algorithm)

 버디 블럭 알고리즘은 크게 아래와 같은 단계로 수행된다(예를 든것이다. 숫자에 너무 연연하지 말자).

  • 초기화 (Initialize)
    • 메모리 영역을 적당한 크기로 나눈다(4Kbyte라고 가정하자) . 나누고 나니 n개 생겼다.
    • 연속된 4Kbyte 블럭들 2개를 합하면 하나의 8Kbyte 블럭을 만들 수 있다. 이것을 전체 영역에 반복하니 (n/2)개가 생겼다.
    • 연속된 8kbyte의 블럭들을 2개 합하면 하나의 16Kbyte의 블럭을 만들 수 있다. 이것을 전체 영역에 반복하니 (n/4)개가 생겼다.
    • 계속 반복하다 보니 1개의 XKbyte의 블럭이 생겼다.

 

  •  메모리 할당 (malloc)
    • 4Kbyte 크기의 메모리 할당 요청이 들어왔다.
    • 4Kbyte 크기영역 중에 빈 영역이 있는지 확인한다.
    • 빈 영역이 있으면 할당해준다.
    • 만약 없으면 위의 8Kbyte 블럭을 찾는다.
    • 8Kbyte 중에 빈 블럭이 있으면 그 블럭을 4Kbyte의 2개로 나누고 그중 하나를 프로그램에 넘겨주고 나머지 하나는 4Kbyte 블럭에 달아둔다.
    • 만약 8Kbyte 영역에 없으면 16Kbyte를 보고 하나 할당해서 4Kbyte를 프로그램에 넘겨주고 나머지 4Kbyte와 8Kbyte를 블럭에 달아둔다.
    • 할당 가능할때까지 이를 반복해서 마지막 1개 남은 XKbyte 블럭도 할당 불가능하면 실패한다.

 

  • 메모리 반납 (free)
    • 4Kbyte 크기의 메모리 반납 요청이 들어왔다.
    • 4Kbyte 해당 영역에 블럭을 반납하고 앞뒤 블럭의 상태가 Free인가 확인한다.
    • 만약 Free라면 4Kbyte + 4Kbyte 블럭을 하나로 합쳐서 상위 8Kbyte 블럭으로 반납한다.
    • 8Kbyte 블럭에 반납한 다음 앞뒤 블럭의 상태가 Free인가 확인한다.
    • 만약 Free라면 8Kbyte + 8Kbyte 블럭을 하나로 합쳐서 상위 16Kbyte 블럭으로 반납한다.
    • 위 과정을 합치는 것이 가능할 때까지 반복한다.

 

 위와 같은 알고리즘을 계속하여 반복하므로 가변 길이의 블럭이 할당과 해제를 반복했을 때 발생하는 단편화(Fragmentation)현상을 어느정도 방지할 수 있다. 실제로 이 방법이 Linux 커널에서 2.4 버전 이전 대 까지 사용된 방법이다. 그 뒤로는 슬랩(Slab) 방식과 혼합해서 사용한다던데... 분석을 열심히하지 않아서 잘... ㅎㅎ...(좋은 자료 있으면 방명록이나 덧글로.. ㅎㅎ)

 

 말로 설명하기 복잡한데, 위의 글을 그림으로보면 이해하기 쉽다. 시스템 전체 메모리가 12Kbyte 정도이고 최초/최대 할당 크기가 1Kbye에서 4Kbyte까지라면 초기 메모리 블럭은 아래와 같은 구성으로 되어있을 것이다(파란색으로 체워진 블럭은 Free 상태의 블럭이다).

 Buddy1.PNG

<버디 블럭 초기화>

 

 이 상태에서 1KB 블럭을 2개 할당받으면 1Kbyte에 블럭이 없고 2Kbyte에도 블럭이 없기때문에 4Kbyte짜리 블럭 하나를 쪼개 2Kbyte 2개로 나누게 된다. 그 중에 한 블럭을 다시 1Kbyte 2개로 나누게 되고 각각을 할당해 주게 된다. 이때 할당받은 각각의 블럭을 A와 B라고 하자. 이것을 그림으로 표현하면 아래와 같다(붉은색 블럭은 Alloc 상태의 블럭이다).

Buddy2.PNG

<1Kbyte 크기의 블럭 2개 할당>

 

 이제 전체 시스템에는 4Kbyte 2개와 2Kbyte 1개가 남은 상태이다. 다시 1Kbyte 하나 할당 받으면 1Kbyte 블럭에는 여유분이 없으므로 2Kbyte 블럭을 1Kbyte 2개의 블럭으로 나누고 그중 하나를 할당하게 된다. 이때 할당받은 블럭을 C라고 하자. 이것을 그림으로 표현하면 아래와 같다.

Buddy3.PNG

<1Kbyte 블럭 1개 추가 할당>

 

 어느덧 시간이 흘러 할당한 블럭의 사용이 끝나서 블럭 C를 반납했다. C블럭을 반납하고 나면 바로 옆의 블럭이 Free 상태의 블럭이므로 이 두 블럭을 합하여 상위 2Kbyte 블럭을 만들 수 있다. 이것을 그림으로 표현하면 아래와 같다.

Buddy4.PNG

<1Kbyte 블럭 1개 반납>

 

 이제 시스템에는 2Kbyte 1개의 블럭과 4Kbyte 2개의 블럭이 Free 상태로 존재한다. 때마침 할당되었던 A와 B 블럭이 반납되었다. 그러면 각각 반납한 후 서로의 블럭을 합하면 하나의 2Kbyte 블럭을 생성할 수 있으므로 합쳐 2Kbyte 블럭 하나를 구성한다. 이때 다시 우측을 보면 2Kbyte 블럭이 인접하여 존재하므로 다시 이 둘을 합하여 상위의 4Kbyte 블럭을 만들 수 있다. 이 수행이 끝나면 다시 초기상태와 같은 4Kbyte 블럭 3개가 존재하게 된다.

 Buddy5.PNG

<1Kbyte 블럭 반납>

 

 Buddy6.PNG

<1Kbyte 블럭 반납-블럭 통합> 

 

 

Buddy1(1).PNG

<블럭 통합 완료>

 

  모든 작업이 끝나고 블럭이 초기상태로 복원되었다. 이것으로 메모리 할당과 해제가 끝났다.

 

4.구현

4.1 00Kernel/Custom/Memory.h/c

 버디 블럭 알고리즘은 할당 가능한 메모리의 최소 크기와 최대 크기를 정의해야한다. 우리는 메모리 할당의 최대크기를 4Mbyte(빈 영역의 최대공간)로 하고 최소 크기는 512Byte로 하자. 그럼 최대 크기와 최소 크기 사이가 총 13단계로 나누어지게 된다. 그럼 메모리 공간을 나타내기위한 14개의 영역관리가 필요하다는 결론이 나오는데, 비트 마스트(Bitmask)를 이용해서 관리하자.

 버디 블럭을 구현하는 방법은 몇가지가 있는데, 한가지는 링크드 리스트(Linked List)를 이용하는 방법이고, 그 다음 방법은 비트 마스크(Bitmask)를 이용하는 방법이다. 리스트를 사용하는 것이 좀더 쉬울 수 있으나 효율면에서는 비트마스크를 사용하는 것이 좋다. 프레임워크 소스 코드는 비트마스크를 사용하여 구현되었다(리스트를 사용하는 방법은 옛날에 한번 해봤다는 지극히 개인적인 이유.... ㅡ_ㅡ;;;).

 

 비트 마스크를 이용하기로 했으니, 비트마스크의 총 크기를 알아야 한다.

 4Mbyte 영역을 512byte 블럭으로 나누면 총 8192개가 나온다. 한 바이트가 8bit이므로 8로 나누면 1024Byte 즉 1Kbyte가 필요하다. 그렇다면 1024byte 블럭은 얼마 크기의 비트마스크가 필요할까? 블럭 크기가 512Byte의 2배이므로 512Byte가 필요하다. 2048byte(2Kbyte)의 블럭은? 256Byte 크기의 비트마스크가 필요하다. 이를 반복하면 각 메모리 블럭당 필요한 바이트 수를 구할 수 있다.

 위의 계산방법을 통해 총 필요한 단계와 각 단계에 필요한 비트 마스크 배열의 크기를 계산하고, 블럭 해제시에 사용할 할당된 크기값을 저장하기위한 배열을 추가하여 동적 할당 구조체를 만들자.

  1. // 동적 할당을 정보를 저장할 구조체
    // 최대 비트마스크의 크기인 MEMORY_MAX_BITMASKCOUNT개의 크기로
    // MEMORY_MAX_BITMASKLISTCOUNT개 만든다.
    // 메모리 용량을 줄이고 싶으면 아래를 튜닝하면 된다.
    typedef struct memoryStruct
    {
        DWORD vdwSizeArray[ MEMORY_ALLOC_MAX_SIZE / MEMORY_ALLOC_MIN_SIZE ];
        BYTE vvbBitMask[ MEMORY_MAX_BITMASKLISTCOUNT ][ MEMORY_MAX_BITMASKCOUNT ];
    } MEMORY, * PMEMORY;

 

 구조체를 만든 다음 함수가 몇가지 필요하다.

  • 버디 블럭 알고리즘이 정해진 블럭의 크기만큼 Align하여 할당하므로 메모리 할당 요청시에 적당한 크기의 블럭을 찾는 함수
  • 비트 마스크를 조작하는 함수
  • 버디 블럭 알고리즘으로 블럭을 할당하고 해제하는 함수

 

 알고리즘이 나름 복잡한 관계로 함수 원형만 추출했다. 구현에 대한 실제 코드는 첨부에서 받을 수 있다.

  1. void InitMemory( void );
    void* AllocMemory( int iSize );
    BOOL FreeMemory( void* pvAddress );
  2. int AllocBuddy( int iAlignSize );
    BOOL FreeBuddy( int iListIndex, int iMaskOffset );
    int GetAlignSize( int iSize );
    int GetListIndexOfMatchSize( int iSize );
    BOOL IsValidListIndex( int iIndex );
    BYTE* GetMaskArray( int iListIndex );
    int FindFreeOffsetInMask( int iListIndex );
    BOOL SetFlagInMask( int iListIndex, int iOffset, BYTE bFlag );
    BYTE GetFlagInMask( int iListIndex, int iOffset, BYTE* bFlag );

 

4.2 00Kernel/Custom/KShell.h/c

 이제 실제로 테스트를 해볼 차례다. KShell.c 파일을 열어서 Shell() 함수에 Memory 초기화 함수를 추가한다.

  1. /**
        KShell 의 Main
    */
    void Shell()
    {
        InitScheduler();
        InitMemory();
        InitSemaphore( &gs_stSema, 1 );
  2.     // 새로운 태스크 등록
        AddTask( EdgeDraw );
        EnableScheduler( TRUE );
       
        ShellLoop();
  3.     while( 1 );
    }

 

 그리고 테스트를 위해 ProcessCommand() 함수에 malloc 명령과 free 명령을 추가한다. malloc 명령은 malloc 0x20 과  같이 사용되며 특정 크기의 블럭을 할당 받을 때 사용한다. free 명령은 malloc 명령으로 할당받은 메모리를 그대로 해제하는 역할을 한다(기능을 추가할수록 코드가 점점 길어지는.. ㅡ_ㅡ;;;). 그리고 마지막으로 동적할당 비트 마스크에 대한 정보를 표시하는 DumpMemory() 함수를 추가한다.

  1. /**
        엔터가 쳐졌으면 버퍼의 내용으로 명령어를 처리한다.
    */
    void ProcessCommand( int* piX, int* piY, char* vcCommandBuffer,
                         int* piBufferIndex)
    {
        char vcDwordBuffer[ 8 ];
        static DWORD vdwValue[ 10 ];
        static int iCount = 0;
  2. ...... 생략 ......

  3. // 메모리를 할당받는다. malloc 00000000의 형태여야 한다.
        else if( ( *piBufferIndex > 7 ) &&
                 ( kMemCmp( vcCommandBuffer, "malloc", 6 ) == 0 ) )
        {
            // 라인을 변경한다.
            NewLine( piX, piY );
            vcCommandBuffer[ *piBufferIndex ] = '\0';
           
            // 할당할 크기 16진수
            vdwValue[ iCount ] = kAToD( vcCommandBuffer + 7 );
            kDToA( vcDwordBuffer, vdwValue[ iCount ] );
            printxy( 0, *piY, "size" );
            printxyn( 10, *piY, vcDwordBuffer, 8 );
  4.         // 할당된 주소
            vdwValue[ iCount ] = ( DWORD ) AllocMemory( vdwValue[ iCount ] );
            kDToA( vcDwordBuffer, vdwValue[ iCount ] );
            printxy( 20, *piY, "Address" );
            printxyn( 30, *piY, vcDwordBuffer, 8 );
            iCount++;
        }
        // 메모리를 해제한다.
        else if( ( *piBufferIndex == 4 ) &&
                 ( kMemCmp( vcCommandBuffer, "free", 4 ) == 0 ) )
        {
            // 라인을 변경한다.
            NewLine( piX, piY );
            if( iCount > 0 )
            {
                iCount--;
            }
            kDToA( vcDwordBuffer, vdwValue[ iCount ] );
            printxy( 0, *piY, "Address" );
            printxyn( 15, *piY, vcDwordBuffer, 8 );
            FreeMemory( ( void* ) vdwValue[ iCount ] );
        }
        // Bitmask를 덤프한다.
        else if( ( *piBufferIndex == 7 ) &&
                 ( kMemCmp( vcCommandBuffer, "dumpmem", 7 ) == 0 ) )
        {
            DumpMemory( piX, piY );
        }
    }
  5. ...... 생략 ......
  6. /**
        Memory의 상태를 Dump해서 표시한다.
    */
    void DumpMemory( int* piX, int* piY )
    {
        int i;
        int j;
        BYTE* pbMask;
        char vcDwordBuffer[ 8 ];
        int iFindCount;
  7.     // 블럭중에 Free 한 것을 Dump 한다.
        for( j = 0 ; j < MEMORY_MAX_BITMASKLISTCOUNT ; j++ )
        {
            NewLine( piX, piY );
            kDToA( vcDwordBuffer, MEMORY_ALLOC_MIN_SIZE << j );
            pbMask = gs_stMemory.vvbBitMask[ j ];
            printxyn( 0, *piY, vcDwordBuffer, 8 );
            iFindCount = 0;
  8.         for( i = 0 ; i < MEMORY_MAX_BITMASKCOUNT * 8 ; i++ )
            {
                if( pbMask[ i / 8 ] & ( 0x01 << ( i % 8 ) ) )
                {
                    kDToA( vcDwordBuffer, i );
                    printxyn( 10 + iFindCount * 10, *piY, vcDwordBuffer, 8 );
                    kGetCh();
                    NewLine( piX, piY );
                }
            }
        }
    }

 

4.3 00Kernel/makefile 수정

4.3.1 프레임워크 1.0.3 이전 버전

 makefile은 깔끔히 정리된 버전을 기준으로 설명한다. 새로운 버전을 받지 않았으면 21 OS 프레임워크 소스 릴리즈에 가면 새로운 makefile을 받을 수 있다.

 새로운 makefile에 OBJ 부분을 아래와 같이 수정한다.

  1. #Object 파일 이름 다 적기
    #아래의 순서대로 링크된다. 새로운 파일이 생기면 뒤에 다 추가하자
    OBJ = Asm.o Kernel.o Isr.o Descriptor.o Interrupt.o Keyboard.o StdLib.o Task.o \
          FrameWork.o KShell.o Scheduler.o Synchronize.o Memory.o

 

4.3.2 프레임워크 1.0.3 이후 버전

 프레임워크 1.0.3 버전 이후는 makefile에 다 통합되어있다. Custom 폴더에 Memory.h/c 파일을 넣고 make를 입력하는 것으로 끝이다.

 

5.Build 및 실행

 위의 모든 과정이 끝난 후 makekernel.bat 와 makeimg.bat 또는 make를 실행하면 커널이 build되고 Virtual Box로 결과를 확인할 수 있다. 아래는 실행 후 dumpmem 명령을 통해 비트 마스크의 내용을 확인한 화면이다. 맨 마지막에 0x400000 크기의 블럭이 0x00000000 인덱스에 존재한다. 이것으로 보아 4Mbyte 크기의 블럭이 정상적으로 할당되어 있음을 알 수 있다.

memory1.PNG

<초기의 메모리 비트마스크>

 

 

 아래는 0x20 크기의 메모리를 할당 받은 후에 다시 메모리 상태를 본 것이다(다 표시되지는 않았지만 0x200 크기의 블럭부터 상위 0x200000 크기의 블럭까지 모두 하나씩 할당되었음을 알 수 있다).

 memory2.PNG

<0x20 크기의 메모리블럭 할당 후>

 

 

 하나를 할당받은 후에 다시 반납하여 메모리 상태를 덤프한 것이다. 알고리즘이 정상적으로 동작하여 다시 0x400000 크기의 블럭이 생성되었음을 알 수 있다. 이를 반복해서 테스트하면 된다.

memory1.PNG

<0x20 메모리 블럭 해제 후>

 

6.마치면서...

 동적 메모리 할당에 대해 구현하면서 멀티 태스킹 시에 발생할 수 있는 문제에 대한 고려는 제외하였다. 동적 할당에 대한 내용만으로도 충분히 복잡하기 때문에, 동기화 부분에 대해서는 다음에 다룰 것이다.

 오늘 이후로 커널 코드에서 마음껏 malloc과 free를 사용할 수 있게 되었다. 비록 블럭 단위가 커서 효율성에 문제가 좀 있지만 프로그램을 주의해서 작성한다면 충분히 감당할 수 있는 부분이라 생각한다.

 

 메모리 공간의 낭비가 마음에 걸린다면 동적 할당 알고리즘을 수정하면 된다. 버디 블럭의 크기를 512Byte 이하로 낮추는 것은 비트마스크의 크기가 커지므로 좋은 방법이 아니다. 괜찮은 방법은 아주 작은 메모리 블럭 같은 경우, 4Kbyte 크기를 버디 블럭으로 할당 받은 후 내부적으로 다시 비트 마스크를 사용하여 나누어 사용하는 것이다. 구현은 각자 해보도록 하자.

 

다음 번에는 멀티 태스크 환경에서 사용할 수 있도록 세머포어를 사용하여 동기화를 해보도록 하자.

 

7.첨부

7.1 프레임워크 1.0.3 버전 이전

 

 

7.2 프레임워크 1.0.3 버전 이후

 

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

Part15. Tutorial3-동기화(Synchronization) 기능을 추가해 보자

원문 :  http://kkamagui.springnote.com/pages/368015

 

들어가기 전에...

0.시작하면서...

 앞서 멀티 태스킹 기능을 추가하면서 동기화가 얼마나 중요한지 느꼈을 것이다. 잘 모르겠다면 커널 크래쉬 화면을 한번 더 보고 오자. 단순히 3개의 태스크만 실행되는 스케줄러를 구현하는데도 동기화 문제가 발생하니, 윈도우와 같이 수백개의 태스크가 동작하는 OS에서 동기화는 PC의 CPU 만큼이나 중요한 문제다.

 효율적인 동기화를 위해 몇가지 동기화 오브젝트를 설계하고 구현해보자.

 

1.동기화 객체(Synchronization Object)

 이번에 구현해볼 동기화 객체는 세머포어(Semaphore)와 뮤텍스(Mutex)이다. 사실 뮤텍스는 카운트가 1인 바이너리 세머포어와 기능이 비슷하기 때문에 세머포어를 구현하고 그것을 이용해서 뮤텍스를 구현하도록 하자. 뮤텍스는 소유의 개념이 포함되어있어 바이너리 세마포어와는 차이가 있지만  세부적인 내용은 넘어가도록 하자(궁금한 사람은 구글링을 @0@)/~)

 윈도우 프로그래밍을 어느정도 해본 사람, 혹은 스레드 프로그래밍을 어느정도 해본 사람이라면 세머포어와 뮤텍스에 대해서 이미 알고 있을 것이다.

 

 그래도 혹여나 모르는 사람들이 있을까봐 간단히 용도를 설명하겠다.

 여러 태스크에서 하나의 공유된 자원(글로벌 변수 or IO 장치 등등)에 접근하게되면 서로 경쟁을 하게 된다. 이 공유된 자원을 순차적으로 사용하고 반납하게 되면 자원에 동시에 접근했을 때 발생하는 문제를 해결할 수 있고, 태스크 간의 "순차적인 사용"을 컨트롤 하기위해 세머포어나 뮤택스를 사용한다.

 앞서 Tutorial에서 멀티태스크의 기능을 구현할 때 스케줄러의 예를 보면 잘 알 수 있다. 글로벌 구조체인 gs_stScheduler가 태스크 간에 공유된 자원이었고 무차별하게 접근할때 커널 크래쉬가 발생했다. 이것을 kLock()과 kUnlock() 메소드를 사용하여 태스크 간의 접근을 컨트롤하였고 스케줄러가 정상적으로 동작할 수 있었다. 잘 기억이 안나는 사람은 Part14. Tutorial2-멀티 태스킹(Multi Tasking) 기능을 추가해 보자를 다시 보도록 하자.

 

2.세머포어(Semaphore) 설계

 세머포어는 크게 카운팅 세머포어(Counting Semaphore)와 바이너리 세머포어(Binary Semaphore)로 나뉘어진다. 두 세머포어의 차이점은 자원에 진입할 수 있는 태스크의 수가 단 한개만 가능한가, 아니면 n개가 가능한가 차이밖에 없다. 세머포어의 사용법은 동기화가 필요한 영역에 진입할 때 On()을 하고 작업을 완료하면 Off()를 호출하는 방식으로 사용하며 On과 Off 사이의 코드는 세머포어의 종류에따라 하나의 태스크만 실행가능하거나 n개의 태스크만 실행할 수 있다.

 세머포어를 깃발과 방으로을 생각하면 이해하기가 좋다. 어떤 방에 들어가는데, 방에 들어가면 입구에 깃발을 하나 올리고 방에서 나오면 깃발을 하나 내린다. 만약 내가 들어갈 차례인데 깃발이 전부 올려져 있으면 누군가 나와서 깃발을 내릴 때까지 기다렸다가 들어가는 방식이다.

 카운팅 세머포어를 구현하기 위해서는 진입 가능한 태스크의 최대 개수, 현재 진입한 태스크의 수를 포함하는 세머포어 자료구조가 필요하다. 이를 아래와 같이 선언하자.

  1. // Semaphore 구조체
    typedef struct semaphoreStruct
    {
        BOOL bLock;
        int iMaxTask;
        int iCurTask;
    } SEMAPHORE, * PSEMAPHORE;

 중요하게 볼 부분은 bLock 부분인데, 세머포어의 변수들 또한 공유되는 자원이므로 이 자원을 수정할 수 있는가 판단하는 플래그가 필요하다. 이 플래그가 없으면 어떻게 될까? 세머포어의 변수를 설정하고 비교하는 부분 역시 여러 태스크가 접근할 것이므로 역시 엉망이 될 것이다(커널 크래쉬를 잊지말기를 바란다).

 

3.구현

3.1 세머포어(Semaphore) 구현

 위에서 세머포어에 대해 간략히 설계했으므로 이제 코드를 작성해 보자. iMaxTask 변수는 최대로 진입 가능한 태스크의 수로 사용하고 iCurTask는 현재까지 진입한 태스크의 수로 사용한다면, 초기화/On/Off 코드는 아래와 같이 쓸 수 있다.

  1. /**
        Semaphore를 초기화 한다.
    */
    void InitSemaphore( SEMAPHORE* pstSema, int iMaxTask )
    {
        kMemSet( pstSema, 0, sizeof( SEMAPHORE ) );
        pstSema->iMaxTask = iMaxTask;
    }
  2. /**
        Semaphore를 점유한다.
    */
    BOOL OnSemaphore( SEMAPHORE* pstSema )
    {
        // 세머포어 변수에 접근하기위해 접근이 가능한지를 확인하고 접근 가능하면
        // 접근 금지를 설정한 후 변수를 본다.
        while( 1 )
        {
            if( kLock( &( pstSema->bLock ) ) == FALSE )
            {
                SwitchTask();
                continue;
            }
  3.         if( pstSema->iCurTask + 1 <= pstSema->iMaxTask )
            {
                break;
            }
            else
            {
                kUnlock( &( pstSema->bLock ) );
                SwitchTask();
            }
        }
  4.     pstSema->iCurTask++;
  5.     // 세버포어 변수에 대한 접근 금지를 해제한다.
        kUnlock( &( pstSema->bLock ) );
  6.     return TRUE;
    }
  7. /**
        Semaphore를 해제한다.
    */
    BOOL OffSemaphore( SEMAPHORE* pstSema )
    {
        // 세머포어 변수에 접근하기위해 접근이 가능한지를 확인하고 접근 가능하면
        // 접근 금지를 설정한 후 변수를 본다.
        while( 1 )
        {
            if( kLock( &( pstSema->bLock ) ) == FALSE )
            {
                SwitchTask();
                continue;
            }
  8.         if( pstSema->iCurTask > 0 )
            {
                break;
            }
            else
            {
                kUnlock( &( pstSema->bLock ) );
                SwitchTask();
            }
        }
  9.     pstSema->iCurTask--;
  10.     // 세버포어 변수에 대한 접근 금지를 해제한다.
        kUnlock( &( pstSema->bLock ) );
  11.     return TRUE;
    }

  초기화하는 함수는 쉽게 이해가될테니 넘어가고 OnSemaphore()OffSemaphore() 함수를 보자. 낯이 익은 함수가 보이지 않는가? kLock()kUnlock() 함수가 여러번 나오는데, 이 함수는 Atomic하게 동작하는 함수로 아래와 같은 역할을 한다.

  • BOOL kLock( BYTE* pbFlag ) : Atomic Operation으로 pbFlag의 값이 0이면 1로 설정하고 1을 리턴하고, 1이면 0을 리턴
  • BOOL kUnlock( BYTE* pbFlag ) : Atomic Operation으로 pbFlag의 값이 1이면 0으로 설정하고 1을 리턴하고, 0이면 0을 리턴

 위의 함수 설명과 세머포어 코드를 같이 보면 전체적인 흐름을 이해하는데 문제가 없을 것이다. 위의 코드를 순서대로 나열하면 아래와 같다.

  1. kLock()을 호출하여 내가 세머포어 변수를 수정할 수 있는지 확인한다.
  2. TRUE가 리턴되면  다른 태스크가 세머포어 변수를 수정하고 있지 않으므로 세머포어 값을 변경한다.
  3. 변경이 끝나면 kUnlock()을 호출하여 세머포어 변수를 수정할 수 있음을 설정한다.
  4. FALSE가 리턴되면 다른 태스크가 세머포어 변수를 수정하고 있으므로 TRUE가 리턴될 때까지 대기한다.

 

 여기서 잠깐... 세머포어를 구현하는데 꼭 필요한 것이 Atomic Operation이라는 것을 알았다. 그러면 어떻게 Atomic Operation을 구현할 수 있을까? Atomic함을 보장하기위해서 필요한 것은 무엇일까?

 그것은 명령을 수행할 때 도중에 인터럽트되지 않고 처리를 끝내는 것이다. 아래와 같이 인터럽터를 Disable 함으로써 간단히 Atomic Operation을 구현할 수 있다(Single CPU라고 가정한다).

  1. ;  BYTE *pbFlag : 옛날버전
    ;     인터럽터를 Disable 시키고, 플래그를 검사하여
    ;  플래그의 값이 0 이면 1로 증가시키고 1을 리턴하고,
    ;  플래그의 값이 1 이면 0을 리턴한다.
    _kLockOld:
      push    ebp
  2.   mov     ebp, esp
      push    ebx
      pushfd
  3.   ; 플래그의 포인터를 얻는다.
      mov     ebx, dword [ss:ebp + 8]
      ; 인터럽터 Disable
      cli
      mov  al, byte [ds:ebx]
      cmp  al, 0
      ; 일단 FALSE를 셋팅해 놓고
      mov  eax, 0x00000000
      jne  kLockExit
  4.   mov  byte [ds:ebx], 0x01
      mov  eax, 0x01
  5.  kLockExit:
      popfd
      pop     ebx
      pop     ebp
      retn
  6. ;  BYTE *pbFlag : 옛날 버전
    ;     인터럽터를 Disable 시키고 플래그를 검사하여
    ;  플래그의 값이 1 이면 0로 감소시키고 1을 리턴하고
    ;  플래그의 값이 0 이면 0을 리턴한다.
    _kUnlockOld:
      push    ebp
  7.   mov     ebp, esp
      push    ebx
      pushfd
  8.   ; 플래그의 포인터를 얻는다.
      mov      ebx, dword [ss:ebp + 8]
      ; 인터럽터 Disable
      cli
      mov  al, byte [ds:ebx]
      cmp  al, 1
      ; 일단 FALSE를 셋팅해 놓고
      xor      eax, eax
      jne  kUnLockExit
  9.   mov  byte [ds:ebx], 0
      mov  eax, 0x01
  10.  kUnLockExit:
      popfd
      pop  ebx
      pop      ebp
      retn

  주석에 표시된대로 더 이상 사용되지 않는다(프레임워크 처음 릴리즈시 버전). 위에서 보듯 인터럽트를 불가로 설정하고 다시 인터럽터 관련 플래그(EFLAG) 레지스터를 복원하는 방식으로 Atomic을 보장했다. 하지만 이 방법의 문제점은 너무 자주 인터럽트를 불가로 설정하기 때문에 인터럽트 처리가 지연될 수 있다.

 Intel Architecture Manual의 Volume 2 Instruction Set을 보면 Atomic한 처리를 위한 명령어들이 나와있다. lock 명령과 xchg, cmpxchg 명령이 그것이다.

 cmpxchg mem, reg 명령어의 역할은 아래와 같다(자세한 설명은 Intel Architecture Volume 2, Instruction Set 문서를 참고하도록 하자).

  • memal 레지스터의 값이 같음 : reg의 값을 mem에 복사를 하고 ZF 플래그를 1로 설정
  • memal 레지스터의 값이 다름 : regmem의 값을 복사하고 ZF 플래그를 0로 설정

 

 아래는 lock, cmpchg를 이용해서 수정한 코드다.

  1. ;  BYTE *pbFlag :
    ;  플래그의 값이 0 이면 1로 증가시키고 1을 리턴하고,
    ;  플래그의 값이 1 이면 0을 리턴한다.
    _kLock:
      push    ebp
      mov     ebp, esp
      push ebx
  2.   mov  al, 0
      mov  ah, 1
  3.   mov  ebx, dword [ss:ebp + 8 ]
      ; 메모리의 값이 al과 같으면 ah를 메모리에 넣고 zf를 1로 셋팅
      lock cmpxchg byte [ds:ebx], ah
      je  LOCKSUCCESS
      mov  eax, 0
      jmp  LOCKEND
  4. LOCKSUCCESS:  
      mov  eax, 1
      jmp  LOCKEND
  5. LOCKEND:
      pop ebx
      pop ebp
      retn
  6. ;  BYTE *pbFlag : 옛날 버전
    ;     인터럽터를 Disable 시키고 플래그를 검사하여
    ;  플래그의 값이 1 이면 0로 감소시키고 1을 리턴하고
    ;  플래그의 값이 0 이면 0을 리턴한다.
    _kUnlock:
      push ebp
      mov ebp, esp
      push ebx
  7.   mov  al, 1
      mov  ah, 0
  8.   mov  ebx, dword [ss:ebp + 8 ]
      ; 메모리의 값이 al과 같으면 ah를 메모리에 넣고 zf를 1로 셋팅
      lock cmpxchg byte [ds:ebx], ah
      je  UNLOCKSUCCESS
      mov  eax, 0
      jmp  UNLOCKEND
  9. UNLOCKSUCCESS:  
      mov  eax, 1
      jmp  UNLOCKEND
  10. UNLOCKEND:
      pop ebx
      pop ebp
      retn

 

3.2 KShell.c/h 수정

 자 그럼 이제 세머포어를 사용해보자. kShell.c 파일을 아래와 같이 수정한다.

  1. SEMAPHORE gs_stSema;      <= 세머포어 구조체 정의
  2. /**
        KShell 의 Main
    */
    void Shell()
    {
        InitScheduler();
        InitSemaphore( &gs_stSema, 1 );   <= 세머포어 초기화, 하나의 태스크만 실행가능하도록 설정
  3.     // 새로운 태스크 등록
        AddTask( EdgeDraw );
        EnableScheduler( TRUE );
       
        ShellLoop();
  4.     while( 1 );
    }
  5. /**
        글로벌 변수에서 값을 읽어서 문자를 찍어주는 함수
    */
    int gs_iX = 0;
    int gs_iY = 0;
    void Print( BYTE bCh )
    {
        int i;
  6.     printchxy( gs_iX, gs_iY, bCh );
       
        // 약간의 Delay를 위해 사용
        kIdle();
  7.     printchxy( gs_iX, gs_iY, ' ' );
    }
  8. /**
        테두리를 그려주는 태스크
    */
    void EdgeDraw( void )
    {
        int i;
        int j;
        BYTE bCh;
        int k;
        char vcBuffer[ 8 ];
        int iTID;
  9.     i = 0;
        j = 0;
        bCh = 0;
        iTID = GetCurrentTID();
       
        kDToA( vcBuffer, iTID );
  10.     for( k = 0 ; k < 50000 ; k++ )
        {
            printxy( 0, 23 - iTID, "=EdgeDraw Task Work=" );
            printxyn( 20, 23 - iTID, vcBuffer, 8 );
  11.         // 콘솔 테두리를 돌면서 .을 찍는다.
            for( i = 30 ; i < 79 ; i++ )
            {
                // Semaphore 대기
                OnSemaphore( &gs_stSema );
                gs_iX = i;
                gs_iY = 23 - iTID;
                Print( bCh );
                bCh++;
                OffSemaphore( &gs_stSema );
                SwitchTask();
            }
        }
    }

  위에 파란색 부분을 보면 Print() 함수가 새로 정의되었는데, 글로벌 변수 gs_iX, gs_iY에서 값을 읽어 화면에 한 문자를 출력했다가 일정시간 뒤에 다시 공백으로 지우는 역할을 하는 함수이다. EdgeDraw()에서 수정된 부분은 OnSemaphore()/OffSemaphore() 함수를 사용하고 그 안에 글로벌 변수 X,Y에 값을 설정한 후 Print() 함수를 호출한 부분이다.

 각 태스크가 공유 자원인  gs_iXgs_iY에 접근하여 값을 설정하고 Print() 함수를 호출하여 다시 공유자원의 값을 사용하는 구조이다. 이것을 빌드하여 이미지를 만든 다음 Virtual Box에서 실행한 상태에서 starttask 명령을 3번정도 입력하여 화면을 지켜보자.

 멀티세머포어사용.PNG

<Start Task With Semaphore>

  위와 같은 화면이 표시될 것이다. starttask를 3번 사용하여 총 4개의 task를 생성하였고 각각의 태스크는 문자를 보여주고 지워졌다가 다시 다른 문자를 보여주는 아주 깔끔한 화면을 보여준다. 세머포어가 동시에 수행될 수 있는 태스크의 수를 1개로 하여 생성되었기때문에 글로벌 변수에 값을 설정하고 화면에 값을 출력하기까지 다른 태스크가 끼어들지 못하여 이러한 결과가 나온 것이다.

 

 하지만  OnSemaphore()와 OffSemaphore() 함수를 주석처리하고 다시 실행해보자.

멀티세머포어사용안함.PNG

<Start Task Without Semaphore>

 

 절대 일부러 합성한 화면이 아님을 미리 알려둔다. 글로벌 변수에 값을 설정하고 Print() 함수를 호출하는 과정에서 중간 중간에 태스크 스위칭이 되어 다른 태스크가 끼어든 결과이다. 정상적으로 문자가 출력되지 않고 또한 제대로 지워지지도 않는다.

 예제로 들기에는 약간 부족한 면이 있지만 이것으로 세머포어의 역할이나 구현에 대해서 어느정도 감을 잡았으리라 생각된다.

 

4.뮤텍스(Mutex)

 뮤텍스는 바이너리 세머포어와 비슷하다. 다만 다른점은 뮤텍스는 소유의 개념이 있어서 On 시에 태스크 ID를 저장해 두고 Off 시에 태스크 ID를 비교해서 On을 수행한 태스크만 처리가 가능하다.

 

 그럼 어떤 방식으로 뮤텍스를 구현할 수 있을까?  아래와 같이 구현하면 된다.

  • OnMutex() : 위에서 보았던 세머포어 구조체에 TID 필드를 추가하고 OnMutex() 함수를 만든뒤 바이너리 세머포어와 동일하게 구현한다. 그리고 마지막에 TID를 보관한다.
  •  OffMutex() : 가장  먼저 TID를 비교하여 OnMutex()를 실행한 태스크인지 비교한뒤 바이너리 세머포어의 Off 함수와 동일하게 구현한다.

 

 크게 어렵지 않은 부분이므로 각자 구현해 보도록 하자.

 

5.마치며...

 이제 동기화 객체도 생성되었으니 멀티 태스킹 프로그래밍을 마음껏 할 수 있다(정말?? ㅡ,.ㅡ;;;). 부담없이 태스크를 만들고 돌려보자. @0@)/~

 

6.첨부

6.1 프레임워크 1.0.3 이전 

  • Asm.asm : 수정된 kLock(), kUnlock() 함수 포함
  • Custom.zip : kShell, Syncrhonize 파일 포함
  • makefile : Makefile  

 

6.2 프레임워크 1.0.3 이후

 

 

이 글은 스프링노트에서 작성되었습니다.

Part14. Tutorial2-멀티 태스킹(Multitasking) 기능을 추가해 보자

원문 :  http://kkamagui.springnote.com/pages/353530

 

들어가기 전에...

0.시작하면서...

 프레임워크에서 태스크 스위칭(Task Switching)을 구현하는 방법에 대한 내용은 참고. Multi Tasking 구현 방법에 설명해 놓았으니 참고하도록 하고, 여기서는 간단한 스케줄러를 구현하는 것을 목표로 하자.

 

1.태스크 스위칭(Task Switching) 관련 함수

 00Kernel/Custom/KShell.c 파일을 열어보면 기본적인 태스크 스위칭 코드가 포함되어있다.

  1. // Task를 저장한다.
    TASK vstTask[ 2 ];char g_vcPrompt[8] = "[FRAME] ";
    int g_iCurrentTask;
    BOOL g_bScheduleStart = FALSE;
  2. /**
        Timer Callback에서 수행되는 Scheduling 함수
    */
    void Scheduler( void )
    {
        if( g_bScheduleStart == FALSE )
        {
            return ;
        }
        g_iCurrentTask = abs( 1 - g_iCurrentTask );
        kSwitchTask( &( vstTask[ abs( 1- g_iCurrentTask ) ] ),
            &( vstTask[ g_iCurrentTask ] ) );
    }
  3. /**
        KShell 의 Main
    */
    void Shell()
    {
        // Task 설정 kSetupTask( &( vstTask[ 0 ] ), ShellLoop, NULL );kSetupTask( &( vstTask[ 1 ] ), EdgeDraw, NULL );    g_iCurrentTask = 0;
        g_bScheduleStart = TRUE;
  4.     ShellLoop();
    }

 위의 파란색 부분이 스위칭에 관련된 부분이다. Shell() 함수와 Scheduler() 함수를 보면 2개의 태스크를 설정하고 두 태스크를 번갈아가면서 호출하는 것을 알 수 있다. 여기서 눈여겨 봐야 하는 함수 2개는 kSetupTask()kSwitchTask() 이다(참고. 프레임워크 주요 함수들 내용 참고). 

  • kSetupTask() : 태스크 구조체와 태스크의 엔트리 포인트, 그리고 태스크 종료 시 호출될 함수의 엔트리 포인트를 받아서 태스크를 설정하는 역할 수행. 태스크 구조체를 생성하는 함수
  • kSwitchTask() : 현재 수행중인 태스크를 저장할 태스크 구조체와 다음에 실행할 태스크를 로드할 태스크 구조체를 받아서 두개를 스위칭하는 역할 수행. 태스크 스위칭을 수행하는 함수

 kSetupTask() 함수의 마지막 파라메터는 NULL로 설정가능한데, 디폴트 태스크 종료 핸들러를 부르겠다는 의미로 사용된다.

 디폴트로 설정된 태스크 종료 함수는 kEndTask()로 설정되어있고 아래와 같다.

  1. /**
        Task의 종료 처리
    */
    void kTaskEnd( void )
    {
        // 필요한 뭔가를 하자
        while( 1 ) ;
  2. }

 만약 위의 무한 루프 코드를 삭제하면 어떻게 될까? 그럼 kTaskEnd() 함수에서 return을 하게되는데 이때 스택은 Top이 Bottom을 지나가게되고 알 수 없는 곳으로 점프하여 이상한 코드를 실행하게 될 것이다. 운이 좋으면 수십초 정도 지나서 Fault가 발생할 것이고, 운이 나쁘면 점프한 즉시 Fault가 발생하여 커널이 정지 된다. @0@)/~ 

 이런 수습이 불가능한 상황을이 되기전에 막아야 하므로, 디폴트 핸들러는 무한 루프를 실행하여 다른 곳으로 가지 못하게 한다.

 

2.간단한 스케줄러 설계

 자 이제 간단한 스케줄러에 대한 이야기를 해보자. 우리가 만들 스케줄러는 아래와 같다.

  • 라운드로빈(Round-Robin)의 알고리즘을 이용
  • 태스크의 수는 최대 30개(왜? 너무 크면 커널 스택을 넘어서기때문)
  • 태스크 리스트의 형태는 링크드 리스트(Linked List)의 형태
  • 타이머 인터럽트를 이용

 그럼 이제 구현을 위해 수정해야할 부분을 하나하나 살펴보자.

 

3.구현

3.1 Task.c/h 파일 수정

 일단 태스크 구조체를 조금 수정해서 태스크를 링크드 리스트의 형태로 만들 수 있도록 하자. FW/Task.h 파일을 열어서 TASK 구조체가 아래와 같이 되어있는 지 확안하고 그렇지 않다면 수정하자.

  1. // Task에 대한 정보 저장
    typedef struct kTaskStruct
    {
        // Stack의 15DWORD를 Register를 저장하는데 사용한다.
        DWORD vdwStack[ MAX_STACKSIZE ];
  2.     int iTID; 
  3.     struct kTaskStruct* pstNext;
    } TASK, *PTASK;

 언제나 그렇듯이 파란색 부분이 주의깊게 볼 부분이다. 태스크 구분을 위한 iTID를 추가하고 다음 태스크 구조체를 연결하기위한 pstNext 부분을 추가했다.

 

 아래 함수는 FW/Tash.c 파일에 포함된 함수인데, 태스크 구조체, 태스크 시작 엔트리 포인트, 태스크 종료 엔트리 포인트를 받아서 태스크 구조체를 설정하는 함수이다. Task가 종료되었을 때 불리는 함수를 설정하는 부분에 버그가 있어서 붉은색 부분을 수정했다. 붉은색 부분을 확인하자.

  1. /**
        TASK 구조체를 설정한다.
    */
    BOOL kSetupTask( PTASK pstTask, void* pfStartAddr, void* pfEndAddr ){
        DWORD* pdwStackTop;
  2.     // 구조체를 초기화 한다.
        kMemSet( pstTask, 0, sizeof( pstTask->vdwStack ) );
  3.     // Stack의 Top
        pdwStackTop = ( DWORD* ) ( pstTask->vdwStack + ( MAX_STACKSIZE - 1 ) );
  4.     // ESP, EBP, EFLAG, EAX, EBX, ECX, EDX, ESI, EDI 순으로 Push 한다.
        pstTask->vdwStack[ 14 ] = ( DWORD ) ( pdwStackTop - 1 );
        pstTask->vdwStack[ 13 ] = ( DWORD ) ( pdwStackTop - 1 );
        pstTask->vdwStack[ 12 ] = EFLAG_DEFAULT;
  5.     // cs, ds, ss, es, fs, gs는 kernel의 기본값으로 설정해 준다.
        pstTask->vdwStack[ 5 ] = GDT_VAR_KERNELCODEDESC;
        pstTask->vdwStack[ 4 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 3 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 2 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 1 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 0 ] = GDT_VAR_KERNELDATADESC;
  6.     // 마지막으로 Stack의 Return Address를 pfAddr로 설정해 준다.
        pdwStackTop[ -1 ] = ( DWORD ) pfStartAddr;
        // 만약 Task 종료 함수가 설정되지 않으면 Default를 설정
        if( pfEndAddr != NULL )
        {
            pdwStackTop[ 0 ] = ( DWORD ) pfEndAddr;
        }
        else
        {
            pdwStackTop[ 0 ] = ( DWORD ) kTaskEnd;
        }
        return TRUE;
    }

 

3.2 Scheduler.c/h 파일 추가

 Custom/Scheduler.c/h 파일은 스케줄러의 구현이 들어갈 파일이다. KShell.c 파일에 스케줄러를 구현해도 되지만 코드가 길어지고 쉘과는 크게 관계 없는 부분이므로 따로 구현하는게 좋다.

 아래의 코드는 Scheduler.h에 포함된 스케줄러 관련 구조체이다. 스케줄러의 태스크 관리 부분은 태스크의 pstNext 필드를 이용하여 링크드 리스트(Linked List)를 이용해서 구현했다.

  1. // 개수를 너무 크게 설정하면 4M 이상으로 넘어가버린다.
    // 그렇게 되면 커널 스택이 데이터를 덮어쓰게 되서 문제가 발생하므로 적당히 조절해야한다.
  2. #define MAX_TASKCOUNT 30
  3.  
  4. // 스케줄러 구현을 위한 태스크 정보를 포함한 구조체
    typedef struct SchedulerStruct
    {
        int iTaskCount;

        // 리스트의 헤더
        TASK* pstHeader;

        // 플래그 변수들
        BOOL bEnableScheduler;
        BYTE bLock;
       
        // 현재 수행중인 태스크 ID
        int iCurrentTID;

        // 태스크 저장을 위한 공간
        BOOL vbAlloc[ MAX_TASKCOUNT ];
        TASK vstTask[ MAX_TASKCOUNT ];
    } SCHEDULER,* PSCHEDULER;

 

 태스크를 추가하고 삭제하는 것에 대한 세부 내용은 그리 어렵지 않으므로 첨부파일을 살펴보도록 하고, 약간의 트릭이 가미된 부분을 중점적으로 살펴보자.

 스캐줄러를 구현할 때마다 고민하는 부분이기도 한데... 구현에 문제가 되는 부분이 태스크가 자신의 ID를 알아내는 방법이다. 태스크가 자신의 ID 즉 TID가 얼마인지 어떻게 알까? 가장 쉬운 방법은 스케줄러가 현재 태스크의 ID를 글로벌 변수 같은데 저장하고 태스크가 그 변수에 접근해서 읽는 것이다.

 이 방식을 사용한다면 태스크 스위칭 시에 글로벌 변수에서 자기 ID를 읽어온 뒤에 다음 태스크를 검색하여 찾고, 스위칭 하기 전에 글로벌 변수에 다음 태스크 번호로 업데이트한 후 스위칭을 완료해야 한다. 왜냐하면 스케줄러가 호출되고 난 뒤는 이미 다른 태스크로 스위칭된 상태이므로 중단되었다가 복원된 태스크가 갑자기 글로벌 변수에 현재 TID를 바꾼다는 것은 기대하기 힘들다.

 그럼 여기서 발생하는 문제!!! 타이머에서 스케줄러 함수를 호출하고 태스크도 수시로 스케줄러 함수를 호출하면 어떻게될까?

 별 다른 처리를 하지 않았다면... 정답은 "엉망이 된다" 이다. ㅡ_ㅡ;;;;  아래 코드를 한번 보자.

  1. /**
        다른 태스크를 실행시킨다.
    */
    void SwitchTask( void )
    {
        TASK* pstCurTask;
        TASK* pstNextTask;
  2.     pstCurTask = &( gs_stScheduler.vstTask[ GetCurrentTID() ] );
        pstNextTask = pstCurTask->pstNext;
        if( pstNextTask == NULL )
        {
            pstNextTask = gs_stScheduler.pstHeader;
        }
        gs_stScheduler.iCurrentTID = pstNextTask->iTID;
  3.     kSwitchTask( pstCurTask, pstNextTask );
  4. }

 위의 상황을 실제 코드에서 시뮬레이션 한번 해보자. 커널에 총 3개의 태스크가 존재한다고 생각하고 현재 태스크를 T1이라 하고 T1의 다음 태스크를 T2, T2의 다음 태스크를 T3라고 가정하자.

 태스크가 스케줄러 함수를 호출해서 위의 파란라인까지 실행한다음, 타이머에 의해서 다시 스케줄링이 되면 어떻게 될까? 아직 태스크 스위칭 함수가 호출되지 않은 상태에서 글로벌 변수인 gs_stScheduler.iCurrentTID 값이 T2로 바뀐 상태이므로 타이머에 의해서 스케줄러가 호출되었을 때는 T2의 태스크 구조체에 T1의 태스크를 저장하게 되고 복원되어 실행되는 태스크는 T3가 된다.

 순식간에 T2 태스크가 사라져 버렸다. 그리고 다시는 T2 태스크를 볼 수 없을 것이다.

 

 실제로 이 코드를 그대로 돌려보면 아래와 같은 기분좋은(??) 크래쉬 화면을 볼 수 있다(General Fault는 코드 실행 시에 발생한 Exception을 의미한다).

크래쉬.PNG

<프레임워크 크래쉬 화면>

 

 정상적으로 실행하기위해서 코드를 어떻게 수정해야 할까? 해결책은 간단하다. 위 코드를 하나의 태스크만 실행하도록 수정하면 된다. 가장 간단한 방법으로는 위 코드의 시작부터 끝까지를 인터럽트 불가로 설정하면 된다. 이렇게 하면 실행에는 문제가 없지만 스위칭을 할때마다 인터럽트가 불가가되니 인터럽트 처리에 문제가 생길 수 있다(인터럽트 지연이 발생한다).

 그럼 다른 방법은 없는걸까? 조금만 더 생각해 보면 인터럽트를 불가해야 하는 부분을 줄일 수 있다. 크게 두가지 부분으로 나눌 수 있는데, 첫번째 부분은 스케줄링 함수를 중복으로 호출하지 못하게 하여 처리할 수 있는 부분이고, 두번째 부분은 인터럽트 불가로 해결해야하는 부분이다.

  1. /**
        다른 태스크를 실행시킨다.
    */
    void SwitchTask( void )
    {
        TASK* pstCurTask;
        TASK* pstNextTask;
        DWORD dwFlags;
  2.  
  3.     // Scheduler를 다른곳에서 호출 못하도록 Lock을 건다.
        if( kLock( &gs_stScheduler.bLock ) == FALSE )
        {
            return ;
        }
  4.     pstCurTask = &( gs_stScheduler.vstTask[ GetCurrentTID() ] );
        pstNextTask = pstCurTask->pstNext;
        if( pstNextTask == NULL )
        {
            pstNextTask = gs_stScheduler.pstHeader;
        }
        gs_stScheduler.iCurrentTID = pstNextTask->iTID;

  5.     // 플래그 레지스터를 저장하고, 인터럽트를 불가로 설정한다.
        dwFlags = kReadFlags32();
        kClearInt();
  6.  
  7.     kUnlock( &gs_stScheduler.bLock );
  8.     kSwitchTask( pstCurTask, pstNextTask );   
        // 플래그 레지스터를 복원하여 이전 인터럽트 플래그를 복구한다.
        kWriteFlags32( dwFlags );
  9. }

 위 부분에서 푸른색 부분이 태스크 스위칭을 중복으로 허용하지 않으면 해결할 수 있는 부분이다. 주의해서 볼 것은 붉은 색 부분인데, 이 부분은 인터럽트와 관련된 부분으로 인터럽트 불가를 설정한 뒤 스위칭을 하고 다시 원래의 인터럽트 플래그를 복원하는 코드이다.

 이렇게하면 혹시나 현재 저장된 이 태스크가 다시 복원되어 플래그 레지스터를 복원하기 전까지는 인터럽트가 불가가 되는게 아닐까? 너무 위험한 생각이 아닐까?

 그렇지 않다. 태스크 스위칭 함수를 호출했을 때 복원되고 저장되는 레지스터중에 EFLAG 레지스터(인터럽트나 각종 상태가 저장되어있는 레지스터)가 포함되어 있기 때문이다. 다시 말해 태스크 별로 인터럽트 가능/불가 플래그를 가지고 있으므로 다른 태스크를 복원했을 때 복원한 태스크가 인터럽트 가능 상태였다면 EFLAG 레지스터 복구를 통해 자연스럽게 가능 상태로 설정된다.

 만약 이것을 kUnlock() 함수를 호출해서 태스크 스위칭 불가 플래그를 풀어주는 방법으로 구현한다고 생각해보자. 태스크를 복원했을 때 제일 처음 해야 할일이 kUnlock()을 호출하는 일이기 때문에 여러가지 꼼수를 사용해서 이를 처리해야 하는데 굉장히 복잡하다.(한번 상상을 해보자... 어떻게 구현할 것인지.. ㅡ,.ㅡ;;;).

 플래그(EFLAG) 레지스터는 태스크 스위칭을 하면서 복원되기 때문에 아주 간단하게 인터럽트 불가 영역을 줄이면서 동기화의 문제를 해결할 수 있다.

 

 뭐 사실 지금 상황에서 인터럽트 불가 영역을 줄이는게 큰 의미가 있겠냐고 의문을 가지는 사람이 있을지도 모르겠다. 스케줄러 전 영역을 태스크 불가로 만들어도 괜찮겠다고 생각하는 사람은 스케줄러 코드가 수십줄이 아니라 수천줄일 때도 괜찮을지 생각해 보기 바란다. 이런 복잡한 스케줄러 코드에서 스케줄러 함수의 시작부터 끝까지 인터럽트를 불가를 한다면? 결과는 상상에 맡기겠다 @0@)/~

 

 kLock()과 kUnlock() 함수는 Atomic Operation으로 플래그의 값을 변경해주고 그 결과를 리턴값으로 나타내는 함수이다. Atomic Operation은 해당 역할을 끝내기 전에 다른 이유로하여 중단됨이 없다는 것을 보장하는 동작이다. 따라서 세머포어(Semaphore)나 뮤택스(Mutex)와 같은 동기화 객체 구현에 많이 사용된다.

 kClearInt() 함수와 kReadFlags32(), kWriteFlags32() 함수는 플래그(EFLAG) 레지스터와 관련된 함수인데 Intel Architecture에 관련된 부분이라서 자세하게 설명하진 않겠다.  자세한 것은 참고. 프레임워크 주요 함수들과 Part5. Intel Architecture에 대한 소개 문서를 참고하자.

 스케줄러의 전체 파일은 아래에 첨부했다.

 

3.3 KShell.c/h 파일 수정

 테스트용 코드가 삽입되어있던 부분을 스케줄러 파일로 다 옮기고 간단히 수정한다. 쉘의 전체 파일은 아래의 코드 첨부를 통해 다운 받도록 하자.

  1. /**
        KShell 의 Main
    */
    void Shell()
    {
        // 초기화
        InitScheduler();
  2.     // 새로운 태스크 등록
        AddTask( EdgeDraw );
  3.     EnableScheduler( TRUE );    
        // Shell의 실행
        ShellLoop();
  4.     while( 1 );
    }

 

3.4 makefile 파일 수정(프레임워크 1.0.3 버전 이전)

 프레임워크 1.0.3 버전 이전 사용자는 makefile을 수정해 주어야 한다. Scheduler.c/h 파일을 추가했으니 makefile을 수정하자.

  1. # 응용 프로그램 파일
    FW.o : $(CUSTOMDIR)Framework.c
     $(GCC) -o FW.o $(CUSTOMDIR)FrameWork.c
    KShell.o : $(CUSTOMDIR)KShell.c
     $(GCC) -o KShell.o $(CUSTOMDIR)KShell.c
    Sched.o : $(CUSTOMDIR)Scheduler.c $(GCC) -o Sched.o $(CUSTOMDIR)Scheduler.c
  2. #Object 파일 이름 다 적기
    #아래의 순서대로 링크된다.
    OBJ = A.o K.o Is.o D.o Int.o Key.o Stdlib.o Task.o FW.o KShell.o Sched.o

 Scheduler.c 파일을 추가했으니 makekernel.bat를 실행해 보자.

 

4.마치면서...

 이번에 스케줄러 코드를 추가하면서 그 동안 숨겨져왔던 코드들의 버그가 속속들이 드러났다. 디버깅한다고 혼쭐이 났는데... 고생한걸 생각하면 눈물이.. ㅜ_ㅜ...

 기념으로 스크린 샷 하나 올린다. starttask 명령과 showtask 명령이 추가되었다. starttask 함수는 EdgeDraw 태스크를 실행해주는 역할을 하고, showtask 함수는 현재 동작중인 태스크의 개수를 리턴하는 역할을 한다.

멀티태스크.PNG

<멀티 태스킹 실행 화면>

 

 

5.첨부

5.1 프레임워크 1.0.3 버전 이전

 프레임워크 1.0.3 버전 이전 파일이다. 다운 받아서 덮어쓰면 된다(기존 코드의 버그도 같이 수정했다).

  • Custom.zip : Custom 폴더의 파일들
  • FW.zip : FW 폴더의 파일들 
  • makefile : Scheduler 파일 빌드용 makefile 

 

5.2 프레임워크 1.0.3 버전 이후

 프레임워크 1.0.3 버전 이후 파일이다. 다운 받아서 덮어쓰면 된다.

 

이 글은 스프링노트에서 작성되었습니다.

Part13. Tutorial1-프레임워크에 기능을 추가해 보자

원문 :  http://kkamagui.springnote.com/pages/353530

 

들어가기 전에...

 

0.시작하면서...

 프레임워크를 컴파일-링크해서 이미지를 만든 다음 Virtual Box로 실행하면 까마귀 프레임워크가 시작되었다는 말과 함께 아래에 Edge Task가 돌면서 우측 상단에는 타이머 인터럽트(붉은 색으로 계속 화면에 바뀌어 찍히는 것)가 발생되고 있는 것을 볼 수 있다.

 프레임워크_처음_화면.PNG

<프레임워크 실행화면>

 

1.키 입력 및 메시지 출력

 자 그럼 이제 프레임워크에 본격적으로 기능을 몇가지 추가해 보자. 일단 아래에서 돌고 있는 태스크는 무시하고 프레임워크 시작 시에 키 입력을 대기하고 키 입력이 되면 커널 쉘 기능과 태스크를 실행하도록 해보자.

 일단 키 입력을 받아야 하는데, 참고. 프레임워크 주요 함수들의 새창으로 띄워서 보면 키 입력에 관련된 함수를 볼 수 있다. 잘 살펴보면 kGetCh() 라는 함수가 있는데, 이 함수를 이용하면 키를 얻어올 수 있다. 이를 이용해서 키 입력을 대기하였다가 키가 입력이 되면 그 키 값을 화면에 표시하고 정상적으로 쉘과 태스크를 실행하도록 수정해 보자.

 프레임워크 엔트리(void FrameWorkEntry( void )) 함수가 프레임워크에서 처음으로 불리는 함수 이므로 이 부분을 수정하면 된다.

  1. 파일명 : Custom/Framework.c
  2. void FrameWorkEntry( void )
    {
        BYTE bCh;
  3.     // 인사말을 찍는다.
        kPrintxy( 1, 7, "KKAMAGUI OS FrameWork Start" );
  4.     // 키 입력을 대기하고 키가 입력되면 키 값을 찍는다.
        kPrintxy( 10, 8, "Wait For Key Input.... Please Input Anythig...");
        bCh = kGetCh();
        kPrintxy( 10, 9, "Your Input Is[ ]" );
        kPrintchxy( 24, 9, bCh );
  5.     // 간단한 쉘 실행
        Shell();
    }

 위의 파란색 부분을 추가한 뒤, makekernel.bat, makeimg.bat를 차례로 실행시키거나 프레임워크 1.0.3 버전 이상 사용자라면 make를 실행하여 커널 이미지(disk.img)를 생성하자. 그후 Virtual Box에서 이미지를 실행하면 아래와 같이 나타난다.

키입력대기.PNG

<키 입력 전>

 대기 메시지를 표시하고 키 입력을 기다리고 있는 화면이다. 우측 상단에는 타이머 인터럽트가 계속해서 발생함을 표시하고 있다.

 아래는 키 A를 입력한 뒤 화면이다. 입력된 키값(a)이 표시되고  커널 쉘과 태스크가 정상적으로 실행 되었음을 알 수 있다.

키입력후.PNG

<키 입력 후>

 

2.커널 쉘(Kernel Shell) 기능 추가

 커널 쉘 파일은 콘솔을 구현하기위해 약간 복잡하게 되어있는데, 기능을 추가하기 전에 중요한 부분을 먼저 살펴보자.

 커널 쉘은 키보드에서 값을 입력받아서 엔터까지를 한 라인으로 보고 라인을 분석하여 명령을 실행하는 구조로 되어있다. 아래 함수는 계속해서 키를 입력받고 엔터가 입력이되면 라인을 분석해서 명령을 실행하는 부분이다.

  1. /**
        Shell을 처리하는 Loop
    */
    void ShellLoop( void )
    {
        BYTE cCh;
        int iBufferIndex;
        char szCommandBuffer[256];
  2.     // 커서를 설정한다.
        kSetCursor( 0, 8 );
        iBufferIndex = 0;
  3.     // 커맨드 라인을 출력한다.
        PrintCommandLine();
  4.     while(1)
        {
            cCh = kGetCh();
  5.         ProcessKeyCode( cCh, szCommandBuffer, &iBufferIndex);
        }
    }
  6. /**
        키의 입력이 있으면 키 코드를 받아서 처리하는 함수
    */
    void ProcessKeyCode( BYTE cCh, char* pcCommandBuffer, int* piBufferIndex )
    {
        // 엔터나 Back space 같은 경우는 버퍼에 넣지 않고 특별하게 처리한다.
        if( ( cCh == KEY_ENTER ) || ( cCh == KEY_BSPACE ) )
        {
            ProcessSpecialKey( cCh, pcCommandBuffer, piBufferIndex );
            return ;
        }
  7.  // 만약 버퍼가 다 차지 않았으면 화면에 찍고 버퍼에 넣는다.
     if( *piBufferIndex < COMMANDBUFFERSIZE )
     {
            pcCommandBuffer[ *piBufferIndex ] = cCh;
            *piBufferIndex = *piBufferIndex + 1;
            kPrintf( "%c", cCh );
     }
     return ;
    }
  8. /**
        특수키에 대한 처리
    */
    void ProcessSpecialKey( BYTE cCh, char* pcCommandBuffer, int* piBufferIndex )
    {
     int iX;
     int iY;
     
     // 만약 리턴이면 스크롤 시켜 본다.
     if( cCh == KEY_ENTER )
     {
      kPrintf( "\n" );
      
      // 버퍼에 든 명령을 실행
      ProcessCommand( pcCommandBuffer, piBufferIndex );
      *piBufferIndex = 0;
      
      // 커맨드라인을 다시 출력한다.
      PrintCommandLine();
     }
     // 만약 백 스페이스 이면 커서를 하나 지우고 뒤로 민다.
     else if( cCh == KEY_BSPACE )
     {
      kGetCursor( &iX, &iY );
      // 버퍼에 데이터가 입력되어 있으면
      // 커서의 위치를 하나 감소하고
      if( *piBufferIndex > 0 )
      {
       if( iX == 0 )
       {
        iX = 79;
        iY = iY - 1;
       }
       else
                {
        iX = iX - 1;
                }
  9.    // 문자를 살짝 지워준다.
       kPrintchxy( iX, iY, ' ' );
       kSetCursor( iX, iY );
       *piBufferIndex = *piBufferIndex - 1;
      }
     }
    }
  10. /**
        엔터가 쳐졌으면 버퍼의 내용으로 명령어를 처리한다.
    */
    void ProcessCommand(char* vcCommandBuffer, int* piBufferIndex)
    {
        char vcDwordBuffer[ 8 ];
        static DWORD vdwValue[ 10 ];
        static int iCount = 0;
  11.     // 화면 지우는 함수
        if( ( *piBufferIndex == 3 ) &&
            ( kMemCmp( vcCommandBuffer, "cls", 3 ) == 0 ) )
        {
    kClearScreen();
        }
    }

 위에서 중요한 부분은 파란색으로 굵게 표시를 했다. 파란색 표시를 죽 따라가면 대충 어떻게 동작하는지에 대해서 알 수 있으므로 자세한 설명은 생략하고, 우리가 기능을 추가하려면 어디를 고쳐야 하는지 알아보자. 위에 붉은 색으로 따로 표시된 ProcessCommand()라는 함수가 있다.

 이 함수의 아래를 보면 Clear Screen(cls) 명령에 대한 처리가 되어있음을 알 수 있다. 라인에 입력된 키 값이 3개이고 그 값이 "cls" 인가를 비교해서 화면을 지우는 간단한 코드이다. 뭔가 감이오지 않는가? 그렇다. 저 곳에 함수를 추가하면 된다.

 자~ 그럼 메모리 총량을 표시하는 "showmem" 이라는 명령을 추가해 보자. 메모리 총량은 참고. 프레임워크 주요 함수들에 보면 kGetTotalRAMSize()라는 함수로 구현되어있다. 이제 명령라인을 추가할 때다.

  1. /**
        엔터가 쳐졌으면 버퍼의 내용으로 명령어를 처리한다.
    */
    void ProcessCommand(char* vcCommandBuffer, int* piBufferIndex)
    {
        char vcDwordBuffer[ 8 ];
        static DWORD vdwValue[ 10 ];
        static int iCount = 0;
  2.     // 화면 지우는 함수
        if( ( *piBufferIndex == 3 ) &&
            ( kMemCmp( vcCommandBuffer, "cls", 3 ) == 0 ) )
        {
            kClearScreen();
        }
        // 메모리의 정보를 표시한다.
        else if( ( *piBufferIndex == 7 ) &&
                 ( kMemCmp( vcCommandBuffer, "showmem", 7 ) == 0 ) )
        {
            kPrintf( "%X\n", kGetTotalRAMSize() );
        }
    }

 위와 같이 수정한 뒤에 빌드하여 커널을 실행하면 아래와 같은 화면이 나타난다(아래 화면은 cls 명령으로 화면을 지운뒤, showmem 명령을 실행한 것이다).

showmem.PNG

<showmem 명령 실행>

 Virtual Box에 메모리 설정이 8Mbyte로 되어있어서 00000008 이라는 값이 표시되었다.

 

3.기타

 커널에 기능을 추가하다보면 DWORD의 값을 화면에 출력해야 하는 일이 자주 발생한다. 디버깅 시에도 필수적인 부분인데 kPrintf() 함수를 이용하면 편리하게 할 수 있다.

 

4.마치면서...

 이번에는 프레임워크에 직접적으로 기능을 추가하는 부분에 대해서 알아보았다. 내가 만든 쉘 코드나 프레임워크 시작 부분이 마음에 들지 않으면 위의 코드를 참조하여 얼마든지 수정해도 된다.

 다른 새로운 기능 추가는 각자의 몫으로 남겨두고 다음에는 멀티태스킹에 대해서 구현해보도록 하자.

 

5.첨부

 코드 첨부 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part12. 커널(Kernel) 및 프레임워크(Framework) 설명

원문 :  http://kkamagui.springnote.com/pages/348766

 

들어가기 전에...

0.시작하면서...

 이제야 커널을 설명하게 됬다. 커널을 설명하기까지 참 많은 내용을 다룬 것 같은데, 이번에는 간단히 기능적인 소개만 할 것이므로 편하게 읽으면 된다.

 

1.FW 폴더 파일 설명

 프레임워크 소스를 열여보면 00Kernel 폴더 아래에 FW 폴더가 있다. 그 안에 세부파일들이 있는데, 이 폴더 아래의 파일들은 프레임워크를 구성하는 핵심 파일들이다.

 각 파일의 역할은 아래와 같다.

  • asm.asm : Kernel에서 사용하는 어셈블리어 코드 모음. 실제 커널이 1M위치에 로딩되었을 때 제일 먼저 실행되는 kInit 함수 포함.
  • asm.h : C 언어에서 사용할 수 있도록 어셈블리어 함수에 대한 선언
  • DefineMacro.h : 커널에서 사용하는 매크로에 대한 정의
  • DefineStruct.h : 커널에서 사용하는 구조체에 대한 정의
  • Descriptor.c/h : GDT에 대한 설정 및 인터럽터 처리를 위한 Interrupt Descriptor Table(IDT)에 대한 구현
  • GlobalValue.h : 커널에서 사용하는 글로벌 변수들에 대한 정의
  • Interrupt.c/h : 인터럽트 서비스 루틴(ISR)에 대한 구현
  • Isr.asm/h : 실제 Interrupt가 발생했을 때, 1차로 호출되는 ISR 함수 구현
  • Kernel.c : 커널 엔트리 루틴 구현. 프레임워크의 엔트리를 호출
  • Keyboard.c/h : 키보드 드라이버에 대한 구현
  • StdLib.c/h : Standard Library에 대한 구현
  • Task.c/h : 태스크 관리 및 스위칭에 대한 구현

 

2.함수 호출 순서(Call Graph)

2.1 커널 시작 시 함수 호출 순서

 커널이 실행되었을 때, 프레임워크의 엔트리 함수가 불리기까지 과정을 한번 살펴보자.

커널_실행_순서.PNG

<커널 시작시 함수 호출 순서>

 

 Asm.Asm의 kInit() 함수를 시작으로 각 함수가 위와 같은 순서로 호출된다. 맨 마지막에 프레임워크 엔트리 함수를 호출하게 되는데, 프레임워크 엔트리 파일을 수정함으로써 원하는 기능을 추가 할 수 있다.  만약 프레임워크의 기능을 바꾸고 싶으면 위에 흐름을 참조해서 원하는 부분에 기능을 추가하자.

 커널 코드 부분은 C 코드로 되어있고 주석이 어느정도 달려 있으므로 궁금한 사람은 개별적으로 분석하면 된다. 이제 슬슬 커널 프레임워크를 사용해서 커널을 제작하기 시작할텐데, 커널을 제작하면서 많이 쓸만한 함수를 모아서 참고. 프레임워크 주요 함수들에 정리해 두었으니 참고하자.

 이 정도의 함수 정도만 알아도 간단한 커널을 제작하는데 큰 문제가 없을 것이다. 실제로 이보다 훨씬 많은 함수가 있지만 Architecture에 의존적인 부분이 많아서 별로 사용할 일이 없다.

 

2.2 kInit() 함수의 비밀

 프레임워크에서 가장 먼저 실행되는 함수가 Asm.Asm 파일에있는 kInit() 함수라고 하였다. 다시말하면 1M의 주소에 위치하는 함수가 kInit() 함수라는 말인데, 수많은 함수가 커널 링크 시에 합쳐지는데 어떻게 kInit() 함수를 제일 처음으로 위치시킬 수 있을까? 비밀은 makefile에 있다.

 

2.2.1 프레임워크 1.0.3 버전 이전

 makefile을 열어보면 아래와 같은 부분을 발견할 수 있다.

  1. #Object 파일 이름 다 적기
    #아래의 순서대로 링크된다.
    OBJ = A.o K.o Is.o D.o Int.o Key.o Stdlib.o Task.o FW.o KShell.o
  2. Kkernel: $(OBJ)
    ld $(OBJ) -o kkernel.bin --oformat binary -Ttext 0x100000

 위에서 나와있듯 A.o 파일이 가장 처음에 위치시켜 Asm.Asm 파일을 가장 먼저 링크하며, Asm.Asm 파일의 가장 앞에 kInit() 함수를 위치시킴으로써 kInit() 함수를 첫 함수로 링크할 수 있다.

 

2.2.2 프레임워크 1.0.3 버전 이후

  1. ...... 생략 ......
  2. #Object 파일 이름 다 적기
    #아래의 순서대로 링크된다. 새로운 파일이 생기면 뒤에 다 추가하자
    #커널에 꼭 필요한 Object 파일들. ASM.o 파일은 항상 제일 앞에 와야한다. 그 이유는
    #커널의 엔트리포인트가 있는 함수이기 때문이다.
    ESSENTIALOBJ = Asm.o Isr.o
  3. ...... 생략 ......
  4. # 최종 링크
    kernel: $(ESSENTIALOBJ) $(CFILEOBJ)
     @echo "==> Making Kernel..."
     $(LD) $(ESSENTIALOBJ) $(CFILEOBJ) -o kkernel.bin --oformat binary -Ttext 0x100000 @echo "==> Complete"
  5. ...... 생략 ......

 

 

3.Custom 폴더 파일 설명

 00Kernel 폴더 아래에 Custom 폴더는 프레임워크를 사용해서 만든 간단한 커널 템플릿 파일(Framework.c, Framework.h)이 들어있다. 이 폴더는 프레임워크를 이용해서 기능을 추가할때 사용자가 추가한 파일들을 저장하는 폴더로 주로 작업하는 폴더가 될 것이다. 커널 템플릿 파일은 프레임워크 엔트리 함수와 외부 인터럽트 발생시 그것을 처리하는 콜백 핸들러(Callback Handler)로 구성되어있다.

 아래는 Framework.h 파일의 내부이다. 함수 이름만 봐도 함수의 역할을 추측할 수 있다.

  1. void FrameWorkEntry( void );
  2. void kIsrTimerCallBack( void );
    void kIsrKeyboardCallBack( void );
    void kIsrMouseCallBack( void );
    void kIsrCom1CallBack( void );
    void kIsrFloppyCallBack( void );
    void kIsrPrimaryHardDiskCallBack( void );
    void kIsrSecondaryHardDiskCallBack( void );

 

 그리고 아래는 Framework.c 파일의 내부이다.

  1.  /**
        Framework 구현 함수
  2.     Written KKAMAGUI, http://kkamagui.tistory.com */
  3. #include "../FW/DefineMacro.h"
    #include "KShell.h"
  4. /**
        Kernel이 시작되었을 때 다른 정리작업을 마치고 제일 처음 부르는 함수
            수정
    */
    void FrameWorkEntry( void )
    {
        // 인사말을 찍는다.
        printxy( 1, 7, "KKAMAGUI OS FrameWork Start" );
  5.     // 간단한 쉘 실행
        Shell();
    }
  6. /**
        Timer Interrupt의 Callback
            필요한 경우 수정
    */
    void kIsrTimerCallBack( void )
    {
        Scheduler();
    }
  7. /**
        Keyboard Interrupt의 Callback
            필요한 경우 수정
    */
    void kIsrKeyboardCallBack( void )
    {
  8. }
  9. /**
        Serial Com 1 Interrupt의 Callback
            필요한 경우 수정
    */
    void  kIsrCom1CallBack( void )
    {
  10. }
  11. /**
        Floppy Interrupt의 Callback
            필요한 경우 수정
    */
    void kIsrFloppyCallBack( void )
    {
  12. }
  13. /**
        Primary HardDisk Interrupt의 Callback
            필요한 경우 수정
    */
    void kIsrPrimaryHardDiskCallBack( void )
    {
  14. }
  15. /**
        Secondary HardDisk Interrupt의 Callback
            필요한 경우 수정
    */
    void kIsrSecondaryHardDiskCallBack( void )
    {
  16. }
  17. /**
        Mouse Interrupt의 Callback
            필요한 경우 수정
    */
    void kIsrMouseCallBack( void )
    {
  18. }

 프레임워크 엔트리 함수에는 간단히 환영 메시지를 출력하고 커널 쉘(kShell)을 실행하도록 되어있다. 타이머 콜백에는 스케줄러 함수가 등록 되어있는 것을 보아 스케줄러가 동작하고 있다는 것을 추측할 수 있다.

 커널에 기능을 확장하고 싶으면 위와 같이 필요한 부분에 코드를 삽입하여 원하는 기능을 구현할 수 있다.

 

 

4.마치면서...

 간단하게 커널과 프레임워크에 대해서 알아봤다. 다음에는 실제 프레임워크 파일을 수정하여 커널에 기능을 추가해보자.

 

 

5.첨부

 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part11. 커널 로더(Kernel Loader) 설명

원문 :  http://kkamagui.springnote.com/pages/347834

 

들어가기 전에...

0.시작하면서...

 커널 로더(Kernel Loader)는 프레임워크 소스 중에서 가장 CPU Architecture에 의존적이고 복잡한 부분이다. 커널 로더를 이해하려면 Intel Architecture에 대한 이해가 필요한데, 일일이 설명하기에 양이 너무 많다. 따라서 부트 로더와 마찬가지로 기능적인 부분으로 나누어 간략히 설명하겠다.

 커널 로더의 소스는 01Boot 폴더에 01Kloader 에 있다. 소스를 같이 참고하도록 하자.

 커널 로더에서 수행하는 순서는 아래와 같다.

  1. 세그먼트 레지스터의 재설정 
  2. 16bit 모드에서 32bit 모드로 전환 
  3. 커널 코드를 1M 주소 영역으로 이동 
  4. 커널 실행

 이제 각각의 순서에 대해서 살펴보자.

 

1.세그먼트 레지스터의 재설정

 부트 로더에서 커널 로더로 실행이 이행되면 세그먼트 레지스터 설정을 따로 할 필요가 없다. 왜냐하면 부트 로더에서 이미 다 설정해서 넘겨주기 때문이다. 하지만 프레임워크의 부트 로더가 커널 로더를 실행하라는 법이 없으므로 다시 세그먼트를 초기화 해준다.

  1.          ;   일단 세그먼트의 초기화
            mov     ax, cs
            mov     ds, ax
            mov     es, ax

 

2.16bit 모드에서 32bit 모드(Protected Mode)로 전환

 32bit 모드는 보호 모드 또는 Protected Mode라고 불리며 일반 16bit 모드와는 달리 여러가지 권한 설정과 영역 보호를 하드웨어적으로 수행할 수 있는 모드이다. 모든 기재를 사용하면 세세한 권한 설정 및 체크가 가능하지만  프레임워크에서는 이를 간단히 설정하여 사용한다.

 앞서 16bit와 32bit 모드간의 차이에 대해서는 Part5. Intel Architecture에 대한 소개에서 설명했으므로 참고하도록 하고, 지금은 전환에 대한 내용만 소개하겠다. 32bit 전환" href="http://kkamagui.springnote.com/pages/363853">참고. Intel i386 CPU의 16bit->32bit 전환 문서도 같이 참고하면 도움이 될 것이다.

 

2.1 디스크립터(Descriptor) 설정 

 전환을 위해 가정 먼저 해야될 준비는 32bit 모드에서 사용할 코드/데이터/스택 등등의 영역(세그먼트)에 대한 정보를 기술하는 디스크립터(Descriptor)를 생성하는 일이다.

 세그먼트를 세세하게 나누면 유저 모드용/커널 모드용 영역으로 크게 구분하고 다시 각 영역을 코드/데이터/스택 등등의 영역으로 각각 분할하여 처리할 수 있다. 하지만 프레임워크에서는 0 ~ 0xFFFFFFFF 크기의 세그먼트를 잡아서 코드/데이터에 할당하여 이를 간단하게 정의하였다. 이러한 모드를 일반적으로 Flat 모드라고 하는데, 논리주소가 물리주소에 1:1로 대응되고 메모리 관가 편리하므로 간단한 커널에서는 많이 쓰인다.

 

 자 그럼 실제로 디스크립터를 설정하는 부분에 대해서 살펴보자.

  1. gdt:
        ;null describtor
            dw 0
            dw 0
            db 0
            db 0
            db 0
            db 0
        ;새로 옮겨진 커널의 CodeOffset
        kernelCodeOffset equ $ - gdt
            dw 0xffff
            dw 0x0000
            db 0x0000
            db 0x9a
            db 0xcf
            db 0
        ;새로 옮겨진 커널의 DataOffset
        kernelDataOffset equ $ - gdt
            dw 0xffff
            dw 0x0000
            db 0x0000
            db 0x92
            db 0xcf
            db 0
        ;videoMemory
        videoOffset equ $ - gdt
        videoDesc:
            dw 0xffff
            dw 0x8000
            db 0x0b
            db 0x92
            db 0xcf
            db 0
        ;linear모드 데이터
        linearDataOffset equ $ - gdt
        linearDataDesc:
            dw 0xffff
            dw 0
            db 0
            db 0x92
            db 0xcf
            db 0
        ;code describtor
        codeOffset equ $ - gdt
        codeDesc:
            dw 0xffff
            dw 0
            db 0
            db 0x9a
            db 0xcf
            db 0
        ;data 영역의 descriptor
        dataOffset equ $ - gdt
        dataDesc:
            dw 0xffff
            dw 0
            db 0
            db 0x92
            db 0xcf
            db 0
    gdt_end:

 주석이 친절히 달려있으니 해당 디스크립터가 어떤 디스크립터인지 쉽게 파악할 수 있을 것이다 . 위에서 주의해서 볼 것은 디스크립터의 첫번째가 NULL 디스크립터로 전부 0으로 채워 할당한 부분이다. 이 부분은 CPU에 의해 예약된 영역이므로 다른 값으로 초기화 하거나 하면 곤란하다(영영 커널이 부팅되는 모습을 볼 수 없을지도 모른다).

 조금 전에 프레임워크에서 코드와 데이터 2개만 할당해 준다고 했는데, 왜 디스크립터가 6개나 되는 걸까? 주로 사용하는 영역은 코드와 데이터 디스크립터이고 나머지는 부수적으로 사용하는 디스크립터라서 그렇다. 필요에 의해 만든 것이므로 크게 신경쓰지 않아도 된다. 실제 동작을 위해서는 32bit 코드/데이터 디스크립터, 그리고 NULL 디스크립터만 있어도 된다.

 

 각 필드에 대한 의미나 플래그 값들은 Intel Architecture의 Volume 3을 참조하도록 하고 일단 디스크립터 형태에 대해서 잠깐 보고 넘어가자.

디스크립터.PNG

<Segment Descriptor>

 

 디스크립터는 위와같이 8byte로 이루어져있으며 각 속성(코드/데이터)에 맞게 플래그를 설정해주면 된다. 실제로 이 작업은 굉장히 까다로운 작업이며 약간 실수(??)하면 재부팅을 연발한다. 벗어나는 방법은 잘 설정된 코드를 참조하거나 끊임없는 수정을 통해 적절한 값을 찾는 방법말고는 없다.... ㅡ_ㅜ... 위에 디스크립터 구조와 부트 로더 코드를 보면서 비교하면 설정된 값의 의미를 알 수 있으므로 넘어간다.

 디스크립터의 설정이 끝나고나면 기대하던 16bit -> 32bit 전환의 단계가 남아있다.  전환하는 방법은 아주 간단하며 CR0 레지스터에 플래그 하나를 바꿔주는 것으로 전환은 끝이난다. 하지만 그 전에 전환 준비가 좀 까다롭다.

 

2.2 Global Descriptor Table Register(GDTR) 설정

 시스템 전체적인 디스크립터의 테이블을 Global Descriptor Table(GDT)이라고 하는데, 단 하나만 존재하는 영역으로써 코드/데이터/스택 등의 세그먼트에 대한 디스크립터나 태스크에 대한 디스크립터 등등을 저장하는 역할을 한다.

 위에서 디스크립터를 연속적으로 할당하여 값을 설정해 주었는데, 이 영역이 바로 GDT가 되는 것이다. GDT가 따로 존재하는 것이 아니고 연속된 메모리 위치에 디스크립터를 생성하면 그것이 GDT가 된다. 이 GDT 영역을 16bit->32bit 전환 전에 GDT Register에 등록해 줘야 하는데 GDTR은 아래와 같은 구조를 가지는 구조체의 주소를 가진다.

 디스크립터2.PNG

<GDTR 구조체>

 

GDT의 크기와 시작주소를 가지는 간단한 구조체이다. 이 구조체에 값을 설정하고 32bit 모드로 전환했을 때 사용할 디스크립터에 값을 설정하는 코드는 아래와 같다.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   GDT를 설정한다.
            xor     ebx, ebx
            xor     eax, eax
            mov     bx, ds
            shl     ebx, 4
            mov     eax, ebx
            mov     word [codeDesc + 2], ax
            mov     word [dataDesc + 2], ax
            shr     eax, 16
            mov     byte [codeDesc + 4], al
            mov     byte [dataDesc + 4], al
            mov     byte [codeDesc + 7], ah
            mov     byte [dataDesc + 7], ah
           
            ; gdtr 레지스터 설정
            ;lea    eax, [gdt+ebx]
            ;lea 명령은 아래의 2줄과 같다.
            mov     eax, ebx
            add     eax, gdt
  2.         mov     [gdtr + 2], eax
  3. ...... 생략 ......
  4. gdtr:
            dw gdt_end - gdt - 1
            dd gdt

 설정된 gdtr의 값을 GDTR 레지스터에 설정하는 코드는 뒤에 16bit->32bit 모드 전환에서 나온다.

 

2.3 16bit->32bit 모드 전환

 마지막 작업은 인터럽터를 불가로 설정하고 실제 32bit 모드로 변환해 주는 것이다. 인터럽터를 불가로 설정하는 이유는 32bit 모드에서 인터럽트 처리를 위한 기반 작업(Interrupt Descriptor Table 설정과 같은 것들..)이 되어있지 않기 때문이다. 실제로 이 작업은 커널이 완전히 로딩된 후 설정하는 작업이기 때문에 커널 실행 전까지는 인터럽트 불가 상태로 설정해 놓는다.

 실제 모드를 변경하는 코드는 아래와 같다.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ; 인터럽터 불가 설정
            cli
           
            lgdt    [gdtr]   <= 실제로 이부분이 GDT를 로딩하는 부분이다.
            ;   보호모드를 설정하고
            ;   인덱스 2 비트가 Floating Point 비트다.
            ;   Floating Point계산 유닛을 내부 유닛을 사용하도록
            ;   설정한다 그렇지 않으면 7번 인터럽터가 발생한다.
            mov     eax, cr0
  2.         or      eax, 0x80000000
            xor     eax, 0x80000000
            or      al, 1
            or      al, 0x04
            xor     al, 0x04
            mov     cr0, eax
  3.         jmp     codeOffset:code_32
  4. ;   여기에 진입하면 이미 보호모드다. 이제 리얼모드와는
    ;   상관없고 커널이 시작되면 돌아갈수도 없다.
    [bits 32]
        code_32:
            ;   레지스터를 초기화 하고
            mov     ax, dataOffset
            mov     ds, ax
            mov     es, ax
            mov     ss, ax
            mov     gs, ax
            xor     esp, esp
            mov     esp, 0x8ffff
            mov     ebp, 0x8ffff

 위에서 점프 명령을 이용하는 이유는 코드 영역을 설정하는 CS 레지스터 같은 경우는 일반적인 mov 명령으로 값을 바꿀 수 없기 때문이다. 따라서 far jump 명령을 이용해서 CS의 값을 커널 코드 영역을 가리키는 코드 세그먼트의 값으로 바꾸어 준다.

 32bit 영역으로 이동한 후에는 DS, ES, SS, GS, FS와 같은 데이터 관련 레지스터를 모두 커널 데이터 디스크립터로 변경하여 이후 데이터 접근 시에 문제가 발생하지 않도록 한다. 마지막 작업으로 스택을 설정함으로써 32bit 모드로 진입을 끝낸다.

 

3.커널 코드 이동

3.1 A20 Line Enable

 이제 남은 작업은 커널 코드를 1M 주소로 이동해서 재배치 하는 작업이다. 굳이 옮기지 않아도 되지만 옮긴 이유는 1M 이하 영역은 BIOS의 코드도 포함되어있고 DMA 관련 데이터 송/수신 시 사용할 영역을 확보한다는 의미도 있다(구버전의 DMA의 경우에는 1M 이상의 메모리에 접근이 불가능 했다).

 커널 코드 이동은 단순이 커널 존재하는 메모리를 1M 영역으로 복사하는 것을 의미하는 것은 아니다. IBM 호환 PC의 경우 16bit 모드일때 Address Line을 다 사용하지 않기 때문에 Address Line을 확장하도록 설정해 줘야 한다. 즉 확장하지 않으면 1M 이상의 메모리로 접근을 해도 이것이 다시 1M 안쪽의 메모리로 Wrapping되어서 접근된다.

 

 그럼 1M 이상의 메모리로 접근하기위해서는 어떻게 해야 할까? A20 Line을 활성화 하면 된다!!. A20이 무엇인지에 대해서는 http://www.resultspk.net/create_os/os-faq-memory.html#what_is_a20를 참고하도록 하고 1M 이상의 메모리로 접근하기위해 Off 상태로 되어있는 Address Line을 활성화 한다는 정도만 알아놓자.

 

 아래는 A20을 활성화하는 코드이다.

  1. ;   A20 영역을 Enable 시킨다.
    ;   wrapping된 line을 Enable한다.
    enable_A20:
        call    a20wait
        mov     al,0xAD
        out     0x64,al
  2.     call    a20wait
        mov     al,0xD0
        out     0x64,al
  3.     call    a20wait2
        in      al,0x60
        push    eax
  4.     call    a20wait
        mov     al,0xD1
        out     0x64,al
  5.     call    a20wait
        pop     eax
        or      al,2
        out     0x60,al
  6.     call    a20wait
        mov     al,0xAE
        out     0x64,al
  7.     call    a20wait
        ret
  8. a20wait:
    .l0:    mov     ecx,65536
    .l1:    in      al,0x64
        test    al,2
        jz      .l2
        loop    .l1
        jmp     .l0
    .l2:    ret

  9. a20wait2:
    .l0:    mov     ecx,65536
    .l1:    in      al,0x64
        test    al,1
        jnz     .l2
        loop    .l1
        jmp     .l0
    .l2:    ret

 

3.2 커널 이동

 이제 커널을 1M 영역으로 복사하면 끝이다. 코드가 좀 복잡하긴 한데, 주된 흐름은 이동 시작 위치와 목적 위치를 설정해주고 부트 로더에 들어있던 커널 이미지 크기만큼 1M 이상의 영역으로 복사하는 것이다.

  1.         ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   뒷 메모리에 있는 커널을 읽어 들여서 점프 한다.
            ;   일단 movsd 명령이 si 와 di의 값을 하나씩 증가시키게 한다.
            ;   df = 0 이면 증가 1 이면 감소
            cld
            ;   그리고 커널을 1M 메모리 영역으로 올린다.
            ;   커널이 위치하는 세그먼트 정보는 0x07c0:0x01fa에 있다.
            mov     ax, linearDataOffset
            mov     es, ax
            xor     eax, eax
            xor     ebx, ebx
            mov     ax, word [es:0x7dfa]
            sub     ax, 1
            mov     bx, 0x0200
            mul     ebx
            ;   커널 시작 섹터에서 0x0200을 곱하고 0xfe00을 더하면
            ;   커널이 시작되는 주소가 나온다.
            ;   거기로 옮기면 된다
            add     eax, 0xfe00
            mov     esi, eax
            ;   그리고 0x07c0:0x01fc에 커널의 섹터수가 들어있으므로
            ;   그것을 읽고 movsd의 명령으로 4byte씩 전송하므로 512/4 = 128
            ;   을 곱하면 movsd의 명령을 실행할 횟수가 나온다.
            xor     eax, eax
            xor     ebx, ebx
            mov     ax, word[es:0x7dfc]
            mov     bx, 0x80
            mul     ebx
            mov     ecx, eax
            ;   그리고 마지막으로 옮길 부분의 Offset을 구하면 1Mbyte의 위치이므로
            ;   0x100000 의 위치로 옮긴다.
            mov     eax, 0x100000
            mov     edi, eax
            ;   마지막으로 세그먼트를 설정한다. 둘다 linear로 설정한다.
            mov     ax, linearDataOffset
            mov     ds, ax
            rep     movsd

 위의 코드가 끝이 나면 1M 위치에 커널이 복사가 완료된 것이다. 이제 1M로 Jump하면 커널이 실행된다.

 

4.커널 실행

 커널의 실행은 허무할 정도로 간단하다. 아래의 한줄로 끝이난다.

  1.         jmp     kernelCodeOffset:0x100000

 그 이후는 커널이 실행되고 프레임워크 초기화를 수행하는 등등의 작업을 한다. 커널 코드가 이동된 곳은 1M(0x100000)의 위치다. 다시말해 커널 코드를 컴파일해서 링크했을 때 1M의 위치에서 실행가능하도록 해야한다.

 어떻게 1M의 위치에서 실행가능한 코드를 만들어낼까? 답은 링크 옵션(Link Option)에 있다. 프레임워크 소스 파일에서 커널을 생성하는 makefile을 열어보면 알겠지만, ld의 옵션중에 코드를 생성할때 특정 위치에서 실행가능하도록 링크하는 옵션이 있다.

-Ttext 0x100000

 위의 옵션을 사용하면 접근하는 변수, 상수들의 주소가 1M 위치를 Base로 해서 생성되기 때문에 1M의 위치에서 실행가능한 것이다. 만약 커널을 2M 영역으로 이동할 필요가 있다면 위의 부분을 0x200000 수정하고 부트 로더에서 커널을 이동하는 위치를 1M -> 2M로 수정해 주면 2M의 위치에서 커널을 실행할 수 있다.

 

5.물리 메모리 크기 확인

 실제 머신에 사용가능한 물리 메모리의 크기를 알면 커널 동작 시에 좀 더 능동적으로 작업을 수행할 수 있다. 물리 메모리의 크기를 아는 방법은 BIOS 함수을 이용해서 구하는 방법이 있으며, 직접 수작업으로 확인하는 방법이 있다.

 수작업으로 확인하는 방법은 메모리를 1M씩 증가시키면서 해당 주소에 1byte 값을 쓰고 다시 읽어서 쓴 값이 정상적인가를 확인하는 방법이다. 만약 물리 메모리가 사용가능하다면 쓰고 읽었을 때 정상적인 값이 나오지만, 사용이 불가능하다면 틀린 값이 나올 것이다. 실제로 Bellona2 OS(http://www.bellona2.com)가 이러한 방식을 택하고 있으며, 프레임워크에서도 직접 체크하는 방식을 따르고 있다.

 

 아래는 직접 체크하는 코드이다.

  1. ;   1M 이후 영역부터 루프를 돌면서 그 결과값을 저장한다.
    ;   결과값을 저장하는것은 0x7c00 즉 부트 섹터 시작 점에 적는다.
    ;   값은 Double Word로 한다.
    checkMemoryAmount :
            push        ebp
            push        eax
            push        ecx
            push        ebx
            push        gs
            push        es
  2.         mov     ecx, 0x00
            mov     ebx, 0x000000                  
            mov     ax, linearDataOffset
            mov     gs, ax
            mov     ax, videoOffset
            mov     es, ax
  3.     continue :
            add     ebx, 0x100000
            inc     ecx
            ;   메모리 체크 진행상황을 본다.
            ;mov        byte[es:ecx], '.'
            ;   일단 메모리에 무각기로 쓰고
            mov     byte[gs:ebx], 0x03
            ;   다시 메모리에 있는 내용을 비교한다.
            mov     al, byte[gs:ebx]
            cmp     al, 0x03
            je      continue
           
            mov     ebx, 0x7c00
            mov     dword[gs:ebx], ecx
  4.         pop     es
            pop     gs
            pop     ebx
            pop     ecx
            pop     eax
            pop     ebp
            ret

 

6.마치면서...

 이로서 부트 로더와 커널 로더의 설명이 끝이났다. 원래는 CPU 의존적인 부분들이라 설명을 하지 않고 넘어가려 했으나, 그대로 넘어가는 것이 마음에 걸려서 약간 설명을 한다는 것이.... ㅡ_ㅡ;;;;

 역시나 딱딱한 이야기들로 가득찼는데... ㅡ_ㅜ... 다음부터는 프레임워크를 이용하여 커널을 작성하면서 해당 파트에 대한 설명을 하겠다(쓰면서도 지루해 죽을 껏 같았다는.. ㅡ_ㅡ).

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part10. 부트 로더(Boot Loader) 설명

원문 :  http://kkamagui.springnote.com/pages/347742

 

들어가기 전에...

0.시작하면서...

 부트 로더(Boot Loader)는 BIOS로부터 제어를 넘겨받아서 처음으로 실행되는 프로그램이다. 그렇다보니 C Runtime 환경 같은건 전혀 기대할 수 없고 어셈블리어로 구현하는 것이 보통이다.

 부트 로더 코더를 세세하게 설명하면 끝이 없으므로, 간단히 기능적인 관점으로 나누어 설명하겠다.

 부트 로더의 소스는 프레임워크 안에 01Boot 폴더에 00Bootstrap 안에 있다. 소스를 같이 참고하면서 보자.

 

 부팅이 되고나면 부트 로더에서 해야하는 작업은 크게 세가지다.

  1. 코드/데이터/스택 영역 설정 및 초기화
  2. 커널 로더 및 커널의 이미지 로딩
  3. 커널 로더의 실행

 

 그럼 이제 각각에 대해서 알아보도록하자.

 

1.코드/데이터/스택 영역 설정 및 초기화

 부트 로더가 제어를 넘겨받았을 때 레지스터의 상태는 BIOS에서 코드 수행시에 사용되던 값이다. 이대로 사용해도 괜찮지만 BIOS가 여러종류가 있고 각 BIOS 마다 코드가 다르므로 레지스터의 값을 예측할 수 없다. 애매한 상황을 피하기위해 각 레지스터의 값을 새로 설정해 준다.

  1. jmp 0x07c0:start <== 코드 세그먼트 레지스터를 0x07c0로 설정
    start :
            mov     ax, cs
            mov     ds, ax
            mov     es, ax
            ;   스텍의 설정
            ;   함수 호출을 위해 필수
            mov     ax, 0x0000
            mov     ss, ax
            mov     ax, 0xffff
            mov     sp, ax
            mov     bp, ax

 위와 같이 세그먼트 레지스터를 코드 영역과 같이 설정해 주고 스택을 세그먼트의 끝에서부터 자라도록 설정해준다.

 위에서 jmp 0x07c0:start 코드를 볼 수 있는데, 이 코드는 CS 세그먼트 레지스터에 0x07c0을 설정하고 IP 레지스터에 start의 주소를 설정하는 코드이다. CS 세그먼트 레지스터에 0x07c0을 설정하면 어떤 일이 발생하는 것일까? 16bit 모드에서 세그먼트 레지스터의 역할은 32bit 모드의 역할과 다르다.

 16bit 모드의 세그먼트 레지스터의 역할은 Base 주소 역할만 하는데, 주소 계산 방식은 아래와 같다.

실제 주소 = 세그먼트 레지스터의 값 << 4 + 레지스터의 값

 즉 start 코드가 위치하는 실제 주소는 0x7c00  + start의 주소가 되는 것이다.

 

 그렇다면 0x7c00 주소는 무엇일까? 0x7c00의 주소는 BIOS가 디스크로부터 한 섹터를 읽어들여 메모리에 복사하는 위치이다. 다시말해 부트 코드가 0x7c00에 위치하며 BIOS가 0x7c00의 주소로 jump해서 부트 코드를 실행한다. 부트 코드가 정상적으로 실행되기 위해서는 0x7c00에서 실행되도록 컴파일 되어야 함은 두말할 필요도 없다.

 

2.이미지 로딩

2.1 플로피 디스크(Floppy Disk) 분석

 부트 로더의 코드 중에 가장 복잡한 부분이다. 플로피에 저장된 이미지를 모두 로딩하는 역할을 하는 함수인데, 코드를 이해하기 위해서는 플로피 디스크에 대한 지식이 필요하다.

 그럼 간단히 플로피 디스크에 대해 알아보자. 플로피 디스크는 아래와 같은 세가지로 구성된다.

  • 섹터(Sector) : 실제적인 데이터를 저장하는 영역. 일반적으로 512Byte 크기. 1~18번까지 총 18개 섹터가 모여서 하나의 트랙을 구성
  • 트랙(Track) : 섹터가 모여 구성된 영역. 0~79번까지의 총 80개의 트랙이 모여서 하나의 헤드를 구성
  • 헤드(Head) : 트랙이 모여 구성된 영역.  플로피 디스크는 일반적으로 2개의 헤드를 가짐.

  이것을 그림으로 보면 아래와 같다.

플로피구조.PNG

<섹터/트랙/헤드의 구성> 

 

 고용량의 플로피 디스크(1.44Mbyte)는 양면으로 되어있기 때문에 헤드의 값이 2이다. 일단 섹터/트랙/헤드의 구성에 대해 알아보았으니, 섹터를 읽어 데이터를 로드한다고 가정하고 어떤 순서로 로딩하는지 알아보자.

 플로피 디스크에 데이터를 읽고 쓰는 순서는 어떻게 될까? 0부터 끝까지 계속 데이터를 써내려간다고 가정하면 아래와 같은 순서로 읽고 쓰게 된다.포인트는 섹터 -> 헤드 -> 트랙 순으로 증가하는 값이다.

  1. 섹터 1, 트랙 0, 헤드 0 ~ 섹터 18, 트랙 0, 헤드 0 까지 작업
  2. 헤드를 1로 변경하고 다시 위의 1 과정을 반복 
  3. 헤드를 0으로 변경하고 트랙을 1 증가. 다시 위의 1~2 과정을 반복 
  4. 위의 1~3 과정을 트랙 0~79까지 반복

 

2.2 플로피 디스크(Floppy Disk) 제어 코드

 위의 일련의 작업을 코드로 옮긴 것이 아래의 코드다.

  1. ;   플로피로 부터 커널 로더를 읽어 들인다.
    ReadSector:
        push    bp
        push    es
        pushf
       
        mov     ax, 0xb800
        mov     gs, ax
       
        reset:  ;   reset floppy
            mov     ax, 0
            mov     dl, 0               ;   Drive=0(A Drive)
            int     0x13                ;   명령전송
            jc      reset               ;   문제가 생기면 다시 시도 한다.  
            mov     ax, 0x1000;0x07e0   ;   이미지를 읽어들일 segment
            mov     es, ax
            mov     bx, 0
  2.     ;   아래의 부분을 반복한다.
        read:       ;   read 하는 부분
            mov     ah, 2                   ;   bios 명령 포멧 es:bx에 저장한다.
            mov     al, 1                   ;   Load 1 Sector
            mov     ch, byte [TRACKCOUNT]   ;   Cylinder    = 0
            mov     cl, byte [SECTORCOUNT]  ;   Sector 2 부터
            mov     dh, byte [HEADCOUNT]    ;   Head 0
            mov     dl, 0                   ;   Drive 0 (A Drive)
            int     0x13                    ;   명령 전송
            jc      error
  3.         ;   그리고 섹터값을 증가시킨다.
            ;   플레그 레지스터를 저장해서 증가 시키는 루틴으로 인해
            ;   플레그의 값이 바뀌는일이 없도록 한다.
            inc     byte [SECTORCOUNT]      ;   섹터를 1 증가한다.
            cmp     byte [SECTORCOUNT], 19  ;   섹터는 0 부터 18 까지 있으므로
            jb      endRead                 ;   작으면 다음 섹터를 읽고
            mov     byte [SECTORCOUNT], 1   ;   같으면 섹터의 크기를 1로 만들고
            inc     byte [HEADCOUNT]        ;   헤드를 하나 증가 시킨다.
            cmp     byte [HEADCOUNT], 2     ;   헤드는 0 에서 1까지 있으므로
            jb      endRead                 ;   작으면 다음 섹터를 읽고
            mov     byte [HEADCOUNT], 0     ;   같으면 헤드를 0 설정
            inc     byte [TRACKCOUNT]       ;   트렉을 하나 증가시킨다.
            cmp     byte [TRACKCOUNT], 80
            jb      endRead
            ;   트렉이 넘어 섰나 체크는 안해도 된다.
            ;   커널이 1.44 메가를 넘길려고...
       
        endRead:
            ;   한섹터가 0x200 이므로 세그먼트 레지스터를 증가 시킨다.
    mov     bx, es
            add     bx, 0x20
            mov     es, bx
            ;   디버깅
            mov     ax, es
            mov     byte [gs:0x00], ah
            mov     byte [gs:0x02], al
            ;   디버깅 끝
       
            ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   진행상황 표시
            mov     bx, word [LOOPCOUNT]
            mov     byte [gs:402], bl
            mov     byte [gs:403], 0x02
            ;   진행상황 점찍기(일단 주석처리)
            ;mov    bx, word [LOOPCOUNT]
            ;mov    ax, 2
            ;mul    bx
            ;mov    bx, ax
            ;add    bx, 482
            ;mov    byte [gs:bx], '.'
            ;inc    bx
            ;mov    byte [gs:bx], 0x02
            ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
            ;   루프 횟수 계산하기
            mov     bx, 0
            inc     word [LOOPCOUNT]   
            mov     cx, word [LOOPCOUNT]
            cmp     cx, word [ds:0x01f8]
            jb      read
  4.     ; 다 읽었으면 Fdd를 Reset 하고 초기화 시켜둔다.
        reset_end:  ;   reset floppy
            mov     ax, 0
            mov     dl, 0           ;   Drive=0(A Drive)
            int     0x13            ;   명령전송
            jc      reset_end       ;   문제가 생기면 다시 시도 한다.  
       
        popf
        pop     es
        pop     bp
        retn

 

3.커널 로더 실행

  커널 로더를 실행하는 부분은 아주 간단하다. 커널 이미지가 0x10000 주소에 로딩되기 때문에 jump만 하면 된다. 아래는 그 코드이다.

  1.         jmp     0x1000:0 ;0x07e0:0

 

 

4.마치면서...

 이상으로 부트 로더에 대해서 간단히 알아보았다. 어셈블리어 코드 하나하나를 설명하면 좋겠지만 세부적인 내용이 궁금한 사람은 Intel Architecture Manual을 참고하자. 다음에는 커널 로더에 대해 알아보자.

 

 

5.첨부

 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part9. 프레임워크 컴파일 및 링크 방법

원문 :  http://kkamagui.springnote.com/pages/347128

 

들어가기 전에...

0.시작하면서...

 프레임워크는 부트 로더와 커널 로더 그리고 커널의 세가지로 나누어진다고 앞서 이야기 했다. 이제 각각을 컴파일하고 링크하여 커널 이미지를 생성해보자.

 

1.컴파일(Compile) 및 링크(Link) 환경

1.1 프레임워크 1.0.3 버전 이전 사용자

 프레임워크 1.0.3 버전 이전의 사용자는 아래의 배치 파일을 프레임워크 루트 폴더에서 찾을 수 있다.

  • 부트 로더 및 부트 로더 컴파일 및 링크 : MakeBoot.bat
  • 커널 : MakeKernel.bat
  • 커널 이미지 만들기 : MakeImg.bat

 배치 파일로 되어있는데 각 파일을 열어보면 단순한 몇개의 명령으로 되어있다. 만약 위 단계에서 에러가 발생하면 에러메시지가 화면에 출력되게 되고, 해당 열을 찾아서 에러를 수정하고 다시 컴파일 하는 방법으로 작업을 계속 수행하면 된다.

 아마 커널을 자주 수정하게 될 것이므로  MakeKernel.batMakeImg.bat 배치 파일은 마르고 닳도록 입력하게 될 것이다.

 

1.2 프레임워크 1.0.3 버전 이상 사용자

 프레임워크 1.0.3 버전 이상 사용자는 이클립스 환경에서 편리하게 사용하기위해 makefile이 통합되어있다. 빌드하는 방법은 간단히 아래와 같이 입력하면 된다.

make -f makefile 또는 make

 나머지는 makefile이 알아서 처리해준다. 에러가 발생하지 않는다면 disk.img 파일까지 완전히 생성해 줄 것이다. 간혹 종속성의 문제로 인해 제대로 빌드되지 않는 상황이 발생하는데 아래와 같이 입력하여 프로젝트를 깨끗히 정리한다음 다시 빌드를 수행하도록 하자.

make clean

 

1.3 커널 이미지 생성

 커널 이미지 만들기를 수행하면 내부적으로 Boot Image Maker(BIM.exe)라는 파일을 사용하는데, 자작한 프로그램이다. 이 프로그램이 하는 역할은 부트 로더와 커널로더 그리고 커널 이미지를 섹터 크기(512Byte)로 정렬한 다음 하나의 디스크 이미지 파일로 생성하는 것이다.

 부가적인 역할은 부트 로더 영역의 일부를 할애하여 전체 이미지의 크기 및 커널 이미지의 시작 위치 등을 기록하는 것이다. 이 값은 부트 로더가 디스크로부터 메모리에 로딩해야하는 섹터의 크기이며, 커널 로더가 1Mbyte 위치에 재배치해야하는 크기이다.

 BIM 코드는 아직 정리가 안되었기 때문에 추후에 올리기로 하고, 지금은 파일 상에서 어떻게 구성되는지만 확인하고 넘어가자.

 커널이미지.PNG

<Boot Image Maker가 생성하는 이미지의 구성>

 

 위에서 보는 것과 같이 단순히 섹터크기로 맞추어 연결해 주며, 부트 로더의 뒷부분에 전체 이미지의 크기, 커널 시작 섹터, 커널 크기를 넣어주는 역할만 한다.

 

 

2.실제 컴파일 및 링크 화면

2.1 프레임워크 1.0.3 버전 이전 사용자

 아래는 MakeBoot.bat가 정상적으로 실행되었을 때 화면이다.

Makeboot.PNG

<MakeBoot.bat 실행>

 이 화면 외에 다른 메시지가 보인다면 그것은 에러 메시지이므로 에러를 처리하도록 하자.

 

 아래는 MakeKernel.bat가 정상적으로 실행되었을 때 화면이다.

MakeKernel.PNG

<MakeKernel.bat 실행>

 이 화면 외에 다른 메시지가 보인다면 그것은 에러 메시지이므로 에러를 처리하도록 하자.

 

 마지막으로  MakeImg.bat가 정상적으로 실행되었을 때 화면이다.

MakeImg.PNG

<MakeImg.bat 실행>

 이 화면 외에 다른 메시지가 보인다면 그것은 에러 메시지이므로 에러를 처리하도록 하자.

 

2.2 프레임워크 1.0.3 버전 이상 사용자

 아래는 make가 정상적으로 실행되었을 때 화면이다. 프레임워크 1.0.3 이상 버전에 대한 빌드 및 환경 설정은 20 작업환경 설치 문서를 참고하자.

 

 Framework1.PNGFramework2.PNG

<프레임워크 빌드 진행 화면(좌측) 및 빌드 완료 화면(우측)>

 

 

3.커널에 파일 추가 방법

3.1 프레임워크 1.0.3 버전 이전 사용자

 커널에 기능을 추가하다보면 파일을 추가로 더 포함해야하는 경우가 생긴다. 그럴 경우 00Kernel 폴더의 Custom 폴더에 파일을 추가한 뒤에 makefile을 에디터로 열어서 수정하면 된다.

 그럼 어디를 수정해야 할까? 만약 스케줄링 기능을 넣어서 Scheduler.c 파일을 추가해야 한다고 가정하면 아래 부분을 고치면 된다.

  1. # 응용 프로그램 파일
    FW.o : $(CUSTOMDIR)Framework.c
    1. $(GCC) -o FW.o $(CUSTOMDIR)FrameWork.c
  2. KShell.o : $(CUSTOMDIR)KShell.c
    1. $(GCC) -o KShell.o $(CUSTOMDIR)KShell.c
  3. Sch.o : $(CUSTOMDIR)Scheduler.c 
    1. $(GCC) -o Sch.o $(CUSTOMDIR)Scheduler.c 
  4. #Object 파일 이름 다 적기
    #아래의 순서대로 링크된다.
    OBJ = A.o K.o Is.o D.o Int.o Key.o Stdlib.o Task.o FW.o KShell.o Sch.o

  위 처럼 파란색 부분을 추가해서 다시 makekernel.bat를 실행하면 정상적으로 커널에 포함할 수 있다.

 

3.2 프레임워크 1.0.3 버전 이상 사용자

 프레임워크 1.0.3 버전부터는 00Kernel 폴더의 Custom 폴더 및 FW 폴더 밑의 *.c 파일을 모두 찾아서 자동으로 빌드하도록 구성되어있다. 따라서 Custom 폴더 및 FW 폴더 밑에 .c 파일을 생성하고 make를 실행하면 된다.

 

 

4.마치면서...

 프레임워크 각 버전별로 빌드하고 이미지를 생성하는 방법에 대해서 알아보았다. 다음에는 프레임워크의 부트 로더 코드에 대해서 알아보자.

 

 

5.첨부

 

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part8. 까마귀(KKAMAGUI) 프레임워크(Framework) 설치

원본 :  http://kkamagui.springnote.com/pages/345299

 

들어가기 전에...

0.시작하면서...

 프레임워크 릴리즈에 대한 내용은  21 커널 프레임워크 소스 릴리즈 내용을 참고하자. 다음부터는 프레임워크를 사용해서 커널을 만들고 실행하는 것을 주제로 진행할까 한다.

 

이클립스를 이용하면 편리하게 빌드하고 테스트할 수 있데, 이클립스 설치 및 설정에 대한 자세한 내용은 06 이클립스(Eclipse) CDT 설치, 07 이클립스(Eclipse) 단축키 및 환경설정에 대한 문서를 참고하자.

 

프레임워크 빌드 환경에 대한 설정은 20 작업환경 설치 문서를 참고하자.

 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part7. 까마귀(KKAMAGUI) 프레임워크(Framework) 소개

원문 :  http://kkamagui.springnote.com/pages/343186

 

들어가기 전에...

0.시작하면서...

 길고 긴 시간을 거쳐서 드디어 프레임워크(Framework)를 설명할 시간이 왔다. 프레임워크에 대한 이해를 돕기 위해 앞서 여러 파트에서 이것 저것 설명했는데, 설명이 부족했던 부분은 프레임워크 설명을 진행하면서 보충하겠다.

 그럼 지금부터 프레임워크에 대해서 하나하나 알아보자.

 

1.프레임워크(Framework) 블록 다이어그램

 아래는 프레임워크를 구성하는 블록 다이어그램이다.

 프레임워크_블록다이어그램.PNG

<Framework Block Diagram>

 

 프레임워크는 위와 같이 크게 3개의 프로그램으로 구성되어있고, 각 프로그램의 역할은 아래와 같다.

  • Boot Loader : BIOS에서 제어를 넘겨받았을 때, 실행되는 부트 코드(Boot Code). 커널 로더(Kernel Loader)와 커널(Kernel)을 메모리에 로딩하는 역할. 로딩 완료 후 커널 로더로 제어 이관
  • Kernel Loader : 16bit 모드에서 32bit 모드로 전환하는 역할. 커널을 1Mbyte 위치에 재배치 한 후 커널로 제어 이관
  • Kernel : 실제 커널이 존재하는 부분. 메모리/인터럽트/태스크의 초기 설정을 수행하여 태스트들이 정상적으로 동작할 수 있도록 환경 설정 수행. 태스크 동작을 위한 Standard Library 제공

 

2.프레임워크(Framework) 실행

 프레임워크를 실행하면 어떻게 되며, 어떤 상태에서 커널 코드를 작성하게 되는 걸까? 아래는 프레임워크 기본 소스를 그대로 컴파일 했을 때 화면이다(Virtualbox를 이용해서 실행시킨 장면).

Startup.PNG

<Framework 실행 화면>

 멋지지 않는가? 골치 아픈 문제를 다 뒤로하고 컴파일만 하면 간단한 쉘까지 포함한 아주~!! 간단한 커널이 나오게 된다. 만약 여기까지 아무것도 모르는 상태에서 어셈블리어 공부하고 Intel Architecture 공부하는 순서를 밟아서 진행한다면 사람에 따라 다르겠지만 무진장 @0@)/~ 걸린다.

 초반에 작성하는 16bit에서 32bit 전환 코드를 작성하기 위해서 Intel CPU Architecture를 정확히 이해하는데 걸리는 시간만 계산한다 해도 무시 못한다. 항상 겪는 문제지만 이론과 실제는 다르기 때문에 코드를 구현할 수 있는 수준에 오르기까지 또 시간이 걸리고, 구현했다 하더라도 열악한 디버깅 환경으로 인해 각종 fault에 부딪혀 좌절하게 된다(실제로 정석대로 밟아가는 많은 사람들이 이 부분에서 어려움을 겪는다).

 

 프레임워크를 사용하게되면 그러한 초반 로드(Load)를 거의 무시할 수 있기 때문에 실질적인 커널 개발에 시간을 더 많이 투자할 수 있다.

 

 

3.메모리 레이아웃(Memory Layout) 및 커널 재배치(Kernel Relocation)

3.1 메모리 레이아웃(Memory Layout)

 자 그럼 이제 프레임워크가 대충 무슨 기능을 해주는가 알아봤으니, 프레임워크의 메모리 레이아웃(Memory Layout)을 한번 보자. (빈영역이라 표현한 부분은 실제로 아무것도 없는 영역이란 의미가 아니라 어떤 용도로 사용되는지 정확히 알 수 없는 영역이다, BIOS마다 빈 영역의 용도는 다를 수 있다).

메모리_레이아웃.PNG

<Framework Memory Layout>

 

 그림으로 보면 복잡해 보이지만 차근차근 살펴보자. 커널은 1Mbyte 위치부터 시작하고 커널 스택은 4Mbyte가 Bottom이 되서 1Mbyte 영역 방향으로 자란다. 다시 말해 커널 영역은 총 1~4Mbyte 영역이고 1Mbyte 영역부터 커널 코드가 존재하고 스택 영역의 시작은 커널 코드 영역 끝 부분 ~ 4Mbyte 까지라는 것이다.

 커널 코드의 크기가 커지면 사용가능한 스택의 양이 줄어든다는 이야기인가? 이론상으로는 커널이 커지면 스택의 공간이 줄어든다. 이 말은 스택에 공간을 많이 할당할 수록 커널의 코드를 덮어쓸 확률이 늘어난다는 말과 같다.

 하지만 걱정은 잠시 미루어 두자. 실제로 커널 코드를 작성해 보면 커널이 큰 기능을 하지 않는 한 1Mbyte가 잘 넘지 않기 때문에 크게 문제가 없다.

 만약 큰 크기의 어플리케이션을 작업해야 한다면 덮어쓸 가능성이 있지 않는가? 물론 가능성이 높지만 그러한 경우는 커널을 작성할 때 커널 코드 안에 큰 어플리케이션을 밀어넣는 것이 아니라, 어플리케이션을 동적으로 로딩해서 메모리에 배치하고 실행하는 방식으로 해결해야 한다. 큰 어플리케이션 코드가 커널에 있다는 것 자체도 낭비이고 커널은 실제로 꼭 필요한 코드만 포함하는 것이 옳다.

 

3.2 커널 재배치(Kernel relocation)

 메모리 맵을 자세히 살펴보면 커널이 1Mbyte 영역에도 존재하지만 0x10000 영역에도 존재한다. 이것은 부트 로더에서 로딩한 것으로 부트 로더는 16bit 코드이기 때문에 1Mbyte 이상 영역으로 접근하기가 어렵다. 따라서 커널을 일단 1Mbyte 영역 아래에 로딩한 다음 다시 위치를 옮겨야 한다. 아래는 이 과정을 순서대로 나열한 것이다.

  1. 부트 로더가 0x10000 영역에 커널 로더와 커널을 적재한다.
  2. 부트 로더가 커널 로더로 제어를 이관하고 커널 로더를 실행한다.
  3. 커널 로더가 32Bit 모드로 전환하고 0x10000에 로딩된 커널을 0x100000(1Mbyte) 영역으로 이동한다.
  4. 커널 로더가 커널로 제어를 이관한다.

 

 자 그럼, 4Mbyte 이후의 영역은 어떻게 할까? 그것은 쓰기 나름이다. 동적 할당을 위해 영역을 할당할 수 있고, 필요하면 커널 스택 설정을 바꿔서 스택으로 사용할 수도 있다.

 프레임워크에서는 4Mbyte ~ 8Mbyte 영역을 동적 할당 영역으로 사용할 것이므로 참고로 알아두자.

 

4.마치면서...

 프레임워크에 대해서 간단히 알아보았다. 다음에는 프레임워크 컴파일 및 실행환경 설치에 대해 알아보자.

 

5.첨부

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

 Part6. 부팅(Booting) 과정 소개

원문 :  http://kkamagui.springnote.com/pages/342881

 

들어가기 전에...

0.시작하면서...

 우리는 앞서 내용에서 OS를 제작할 때 알아야할 몇가지에 대해서 가볍게(??) 알아봤다. 너무 가벼운 관계로 쪼금 찔리긴 하지만, 일단 그 정도에서 접고 슬슬 프레임워크에 대해서 설명을 하겠다.

 

1.부팅(Booting)이란?

 프레임워크의 동작 과정을 알려면 첫째로 부팅과정에 대해서 알아야 한다. 그럼 도대체 부팅이란 것이 무엇일까?

 우리가 매일 쓰는 네이버의 백과사전을 검색해 보았다.

Booting(부팅)은 컴퓨터의 시동을 뜻하며, 보조기억장치(bootable device:주로 플로피디스크 또는 하드디스크)를 사용하여 컴퓨터가 동작할 수 있도록 시스템에 운영 체제를 불러 들여 작동을 준비하는 작업이다. 즉, 컴퓨터 시스템을 시동하거나 초기 설정하는 것을 뜻한다.
일반적으로 컴퓨터의 전원을 켜면 먼저 부팅 프로그램을 불러 들이고, 부팅 프로그램은 운영체제(Operating System)를 기억장치로 불러 들여 컴퓨터가 작동을 할 수 있도록 준비한다.
처음 사용자가 컴퓨터의 전원을 넣는 것으로 시작하는 부팅을 콜드(cold)부팅이라 하고 원래 전원이 들어와 있는 상태에서 하는 부팅을 웜(warm)부팅이라 한다.

 그렇다. 바로 컴퓨터를 시동하는 작업을 이야기 하는 것이다. 그럼 우리가 PC를 시동하면 어떤일이 발생하는가?

 다들 알겠지만 시커먼 바탕화면에 마치 무슨 콘솔(Console)처럼 흰 글자가 화면에 찍히고 램용량 표시/CPU/HDD 정보 표시 등등이 나타난다. 이 과정을 POST(Power On Self Test)라고 하는데 메인 보드의 BIOS(Basic Input Output System)에서 자체적인 하드웨어 점검을 해주는 것이다. 여기서 이상이 생기면 비프(Beep) 음이 울리게 되고 더이상 진행되지 않는다. 이 테스트에서 별 이상이 없으면 하드디스크를 읽어서 메모리에 적재한 뒤에 OS가 실행되게  된다.

 

2.부팅(Booting) 과정

 그럼 대충 어떤 것인지 알아봤으니 조금더 상세하게 알아보자. 아래는 실제로 부팅이 될때 그 과정을 나타낸 것이다.

 부팅과정(1).PNG

<Boot Process>

 

 

 위에서 보면 알겠지만, BIOS는 Boot Sector를 로딩해주는 것으로 일을 마친다. 다시말하면 Boot Sector부터 커널을 로딩해서 OS를 동작시키는 것은 우리의 몫이다.

 그렇다면 부트 코드(Boot Code)부터 전부 작성해야 한다는 이야기인가? 그렇다. 전부 다 작성해야 한다.

 부트코드를 작성하는 데, 설마 C로 하겠지라고 생각하는 사람은 없을 것이라 생각한다(예전에 아무것도 모를 때, 친구가 베이직으로 OS를 작성할 것이라고 이야기 했었는데... 갑자기 그 생각이 난다).

 생짜로 어셈블리어로 짠다 @0@)/~~~ ㅜ_ㅜ

 

3.부트 코드(Boot Code) 및 커널 로더(Kernel Loader)

 부트코드를 어셈블리어로 작성해야한다는 사실이 초보 레벨의 프로그래머에게 굉장한 부담이 되긴 하지만, 좋은 소식이 있다. 사실 부트 코드가 굉장히 간단하고, 기능이 다 비슷하기 때문에 어느정도 정형화된 코드가 있다.

 또한 한번 만들고 나면 거의 손볼일도 없기 때문에 여기저기 참고해서 만들어도 큰 문제가 없다(배우는 입장에서는 한번 짜보는 것이 당연히!!! 좋다). 부트 코드는 BIOS에 의해서 0x7C00에 로딩되게 되는데, 그렇다면 이 부트코드는 어디에 있는 것일까?

 전통적인 이유로 인해 16bit XT시절부터 지금까지 하드디스크 또는 플로피 디스크의 첫번째 섹터에 부트 코드가 있다. 이런 물리적인 장치의 한 섹터의 크기가 512Byte이므로 부트 코드의 크기도 512Byte로 한정되어있다. 512Byte라니... 이 크기로 무엇을 하란 말인가?

 실제로 부트코드에서 뭐 좀 제대로 일 할려고 하면 512Byte의 크기를 넘는다. 따라서 부트 코드에서는 간단히 이미지를 디스크에서 메모리로 로딩하는 역할만 한다. 그후 커널을 재배치하고 기초적인 환경 설정을 해주는 프로그램을 실행하는데 그것이 커널 로더(Kernel Loader)이다. 커널 로더와 커널은 분리되어있는 경우도 있고 같이 있는 경우도 있는데, 프레임워크에서는 분리하여 구현하였다.

 프레임워크와 같이 분리하여 구현하는 경우 커널 로더가 커널 로딩에 책임을 지게되고 커널 실행을 위한 환경 설정도 책임지게 되므로 부트 코드는 아주 간단해지는 장점이 있다.

 

 

4.커널(Kernel) 실행

 마지막으로 커널 로딩이 끝나고 나면 커널로더에서 커널의 시작 주소로 Jump하여 커널을 실행하고 이제 본격적인 OS의 동작이 시작된다.

 

 

5.마치면서...

 오늘은 간단히 부팅과정에 대해서 알아봤는데, 다음에는 프레임워크에 대해서 좀더 자세히 알아보자.

 

 

6.첨부

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part5. Intel Architecture에 대한 소개

원문 :  http://kkamagui.springnote.com/pages/339549

 

들어가기 전에...


0.시작하면서...

 앞서 인터럽트와 어셈블리어에 대해서 설명을 했다. 이제 프레임워크의 기반 환경인  Intel CPU Architecture에 대해서 알아보자.

 

1.Intel Architecture 다이어그램

 아래 그림은 System Programming 관점에서 본 i386 Architecture에 대한 그림이다. (참고로 여기 나오는 모든 그림은 다 Intel Architecture Manual에서 찾을 수 있다).
 Intel3.png

 

<Intel System Architecture>

 

 

 굉장히 복잡하게 그려져있는데, 프레임워크를 이해하기위해 필요한 부분은 General Register, Segment Register, Descriptor 정도이다.

 

2.레지스터(Register)와 16/32bit 모드(Mode)

 i386 Architecture는 16bit와 32bit 두가지 모드를 가지고 있는데, 16bit모드 일때와 32bit 모드일때 Segment Register의 역할이 다르고 사용가능한 General Register의 크기가 다르다는 차이가 있다. 16bit 모드에 대해서는 궁금한 사람은 Intel Architecture 메뉴얼을 참조 하도록하고 여기서는 32bit 모드에 중점을 두겠다.

 

2.1 레지스터(Register) 종류

 32bit 모드보호 모드(Protected Mode)라고 하는데, 권한에 따른 메모리 보호와 접근 범위 설정이 가능하기 때문이다. 32bit 모드에서 레지스터는 아래와 같이 구성된다.

Intel4.png

<32bit Mode Register>

 

 위의 각 레지스터 역할은 아래와 같다. 

  • General Register : 연산이나 비교, 저장 용으로 사용되는 레지스터
  • Segment Register : Descriptor를 지시하는 역할을 하는 레지스터
  • EFLAGS Register : 비교연산이나 산술연산의 결과의 Overflow, Underflow 같은 연산에 대한 결과 값과 인터럽터 가능/불가와 같은 전체적인 상태값을 저장하는 레지스터
  • EIP Register : 수행중인 명령의 위치를 가리키는 레지스터(Instruction Pointer)

 

 Segment Register는 디스크립터(Descriptor)를 지시하는 역할을 한다고 했는데, 디스크립터에 대해서는 다음에 상세하게 알아보도록 하자.

 

2.2 세그먼트 셀렉터(Segment Selector)

 32bit Mode에서 Segment Register의 역할은 아주 단순하다. 바로 디스크립터를 지시하고 현재 Level(Ring 0, Ring1, Ring2, Ring 3)을 표시하는 역할이다.

 아래는 Intel Architecture에서 Segment Register의 비트별 용도를 나타낸 것이다.

 Intel1.PNG

<Segment Selector>

 

 Bit 3~15까지가 실제 디스크립터의 인덱스를 의미한다. 그렇다면 아래의 3bit의 용도는 무엇일까? 3bit의 용도는 아래와 같다.

  • TI bit : 디스크립터가 전역 디스크립터 테이블(Global Descriptor Table)에 있는지 지역 디스크립터 테이블(Local Descriptor Table)에 있는지를 의미
  • RPL bit : 해당 디스크립터에 접근하기위한 권한 레벨. 만약 RPL이 디스크립터에 설정된 DPL보다 값이 크면(낮은 레벨이면) 디스크립터에 접근 불가

 디스크립터의 인덱스 설정과 동시에 권한을 설정해 주는 필드로 체워져 있음을 알 수 있다. 권한에 따른 보호는 CPL과 RPL 그리고 DPL이 서로 엮여져 있는데 이에 대한 자세한 내용은 참고. Intel i386 CPU의 보호(Protection) 방식을 참고하자.

 

2.3 디스크립터(Descriptor)

 디스크립터(Descriptor)가 무엇인고 하니, 메모리 범위를 지정하고 해당 메모리의 속성을 지정하는 기술자(Descriptor)이다. 언듯 이해가 잘 안갈텐데, 이것을 이해하려면 OS의 메모리 관리 기법중에 세그먼테이션(Segmentation)에 대한 내용을 알고 있어야 한다. 세부적인 내용은 OS책을 보도록 하고 간단히 개요만 설명하겠다.

 

 Intel5.png

<Segment Register와 Segment의 관계>

 

 

 위의 그림이 Segment Register와 디스크립터의 관계를 표시한 것이다. 각 Segment Register는 한개의 디스크립터를 지시할 수 있고,  디스크립터는 코드영역인지, 데이터 영역인지, 스택영역인지에 대한 속성과 범위(시작과 끝)를 가지는 세그먼트(Segment)의 내용을 담고 있다. 즉 OS의 각 영역은 여러개의 조각(Segment)로 구분될 수 있으며, 각각의 세그먼트에 대한 구체적인 내용(속성, 시작, 끝 등등)을 가지고 있는 것이 디스크립터이다.

 하나의 디스크립터는 여러개의 Segment Register에 의해 선택될 수 있고, 극단적인 상황에서 모든 Segment Register가 하나의 디스크립터를 지시할 수 있다.(단 해당 디스크립터의 속성은 그에 합당하는 속성(코드/데이터 속성 등등)을 모두 가지고 있어야 한다.)

 Intel2.PNG

<Descriptor 구조체>

 

 디스크립터는 위에서 보는 것과 같이 시작 및 크기, 속성, 특권 레벨(DPL)로 구성되어있다. 디스크립터에 대한 자세한 내용은 Intel Architecture Manual 및 참고. Intel i386 CPU의 보호(Protection) 방식 문서를 참고하도록 하자.

 

 그럼 Segment와 General Register와 Address 간의 관계는 어떻게 되는걸까? 암묵적으로 Segment Register가 사용되는 부분은 아래와 같다.

1. CS : 코드 수행 시 암묵적으로 사용
2. DS : 데이터 접근 시 암묵적으로 사용
3. SS : 스택 접근 시 암묵적으로 사용

 

 그렇다면 절대 주소(Absolute Address)의 계산은 어떻게 되는걸까?

1. 실제 코드가 수행되는 메모리 절대 주소 : CS가 가리키는 디스크립터의 Base Address + EIP
2. 데이터 접근이 수행되는 메모리 절대 주소 : DS가 가리키는 디스크립터의 Base Address + Memory Address
3. 스택 연산이 수행되는 메모리 절대 주소 : SS가 가리키는 디스크립터의 Base Address + ESP, EBP

 위와 같이 된다. 우리가 접근하는 위치는 디스크립터의 Base Address를 시작으로 하는 위치가 되기 때문에 이를 잘 생각해야 한다. 커널을 빌드하고 이를 실행했을 때 영향을 미치기 때문이다. 프레임워크에서는 단순하게 하기위해 Segment를 크게 2개(코드/데이터)로 구분하고 Base Address를 0, 크기를 4GByte로 설정했기 때문에 커널 코드에서 사용하는 위치는 절대 주소라고 보면 된다.

 

 

2.4 프로세스 모드(Processor Mode)

 자 그럼 여기서 잠깐만 Intel Architecture가 가지는 여러 모드(Mode)에 대해서 집고 넘어가자. 지금 16bit와 32bit 모드만 설명했는데, 2개의 모드가 전부일까?

 그렇지 않다. Intel Architecture Volume3을 참고하면 알겠지만 여러가지 모드가 존재한다.

Intel_Mode.PNG

<Intel CPU Mode 전환>

 

 위의 모드에 대해서 전부 설명하려면 책 한권으로도 부족하므로 궁금한 사람은 Intel Architecture Manual을 참고하도록 하고 간단히 설명만 하겠다.

  • Real-Address Mode : 16bit 모드
  • Protected Mode : 32bit 모드. 프레임워크가 사용하는 모드
  • Virtual-8086 Mode : 32bit 모드에서 16bit 모드를 실행하기위한 에뮬레이션 환경.
  • IA-32e Mode : 64bit 모드 또한 32bit 호환 모드

 우리가 주로 사용하는 모드는 Protected Mode 이고 처음 시스템이 부팅되었을 때 모드는 Real-Address Mode라는 것만 알아두자.

 

 

3.스택(Stack)

 마지막으로 프로그램 제작이든 커널 제작이든 꼭 필요한 스택에 대해서 알아보자.

Intel6.png

<Intel CPU의 Stack 구조>

 

 

 위의 그림에서도 나와있지만 i386의 스택은 높은 주소(0xFFFFFFFF 쪽)를 Base로 하고 낮은주소(0x0000 쪽)로 자라게 되어있다. 그렇기 때문에 앞서 본 호출 규약(Calling Convention)에서도 Parameter 접근시 ebp + 0x08과 같은 숫자를 더하는 방식으로 접근한 것이다.
 커널 프로그래밍을 하면 스택이 어느 방향으로 자라고 Top이 항상 어디에 위치하는지 확실하게 알고 있어야 한다. C와 어셈블리어 간의 함수 호출이라던지, 하드웨어적인 도움을 받지 않고 소프트웨어적으로 하는 태스트 스위칭이라던지 하는 부분은 모두 스택을 사용하기 때문에 스택을 제대로 이해하지 못하면 앞으로 나갈 수 없다.

 스택을 제대로 이해하고 있으면 불가능하게 보이는 일도 가능하게 만들 수 있다(32bit 모드에서 16bit BIOS 함수를 호출하는 것과 같은 일들...). 스택의 개념을 머리속에 항상 새겨놓도록 하자.

 

4.마치면서...

 Intel Architecture에 대해서 간단하게 알아보았다. 다음부터는 프레임워크에 대한 설명을 할 것이므로 지금까지 한 내용을 참고하면서 보면 많은 도움이 될 것이다.

 

 

 

 

이 글은 스프링노트에서 작성되었습니다.

이 글은 스프링노트에서 작성되었습니다.

Part4. 어셈블리어(Assembly Language)와 C 그리고 호출 규약(Calling Convension)

원문 :  http://kkamagui.springnote.com/pages/339546

 

들어가기 전에...

 

0.시작하면서...

OS를 개발하면서 초반에 어셈블리어로 작성한 코드를 보면, 사용한 어셈블리어 명령이 몇 종류 없는 것을 알 수 있다. 그것도 아주 기초적인 수준의 어셈블리어만 사용했는데, 역으로 말하면 몇가지 종류의 어셈블리어만 알고 있으면 부트로더(Boot Loader), 커널로더(Kernel Loader), 그리고 기타 초기화 함수를 작성할 수 있다.

 

1.어셈블리어(Assembly Language) 기초 명령

 아래는 기초 명령의 리스트이다(Intel Style의 명령이라 가정한다).

 

  • mov A, B : B에서 A로 값을 이동
  • cmp A, B : 두 값을 비교하여 결과를 Flags 레지스터에 업데이트
  • rep instruction : insturction을 CX 레지스터의 값 만큼 반복 수행
  • call X : Stack에 Return Address를 삽입하고 jump 수행
  • jmp X : 무조건 해당 주소로 jump
  • je, ja X : 조건 분기 명령. Flags 레지스터의 플레그 값에 따라서 jmp 수행(보통 cmp와 같은 명령어와 함께 사용)
  • push X: 스택에 값을 저장
  • pusha, pushad : 스택에 모든 레지스터 값을 저장. EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI 저장
  • pop X : 스택에서 값을 꺼냄
  • popa, popad : 스택에서 모든 레지스터의 값을 꺼냄. 위의 pushad 명령과 같은 순서의 레지스터 사용
  • add A, B : A에 B의 값을 더함
  • sub A, B : A에서 B의 값을 뺌
  • mul A : EAX의 값과 A의 값을 곱하여 A에 저장
  • inc A : A의 값을 1 증가시킴
  • int X : X번째의 Software Interrupt를 발생시킴
  • ret, retn : Stack에 포함된 Return Address를 꺼내서 해당 주소로 복구(보통 Call 명령과 같이 사용)
  • iret, iretd : 인터럽트 처리 시에 모든 처리를 완료하고 다시 태스크로 복구
  • or A, B : A에 B값을 OR
  • xor A, B : A에 B값을 XOR
  • not A : A의 값을 반전(0->1, 1->0)
  • lgdt : GDT를 설정(Intel Architecture 특수 명령)
  • lidt : IDT를 설정(Intel Architecture 특수 명령)
  • lldt : LDT를 설정(Intel Architecture 특수 명령)
  • ltr : Task Register에 TSS를 설정(Intel Architecture 특수 명령)
  • clts : Task Switching 플래그를 0으로 설정(Intel Architecture 특수 명령)
  • cli : 인터럽트 불가 설정
  • sti : 인터럽트 가능 설정
  • fninit : FPU 초기화 명령(x87 Floating Point Unit 관련 명령)
  • ... 기타 등등

 

 물론 전부를 나열하지는 않았지만 척 봐도 알 수 있는 기본적인 명령어들이다. 물론 성능을 고려한다면 더 많은 어셈블리어 명령어들이 리스트에 포함되겠지만, 성능적인 면을 고려하지 않는다면 위의 함수 정도면 OK다.

 위의 함수에 대한 기본적인 기능들은 Intel Architecture Manual Volume 2 Instruction Set 문서를 참고하면 된다. 위의 명령어를 사용하여 프로그램을 작성하고 싶은 사람은 New Wide Assembler(NASM)을 이용하면 테스트 가능한데, 10 참고자료를 참고하면 간단히 함수를 생성하고 빌드 할 수 있다.

 http://nasm.sourceforge.net/ 홈페이지에 가면 컴파일러를 다운받을 수 있고 예제 및 문서도 제공하므로 한번 해보는 것도 괜찮을 듯 하다.

 

 

2.호출 규약(Calling Convention)

2.1 stdcall, cdecl, fastcall

 실제로 어셈블리어를 아는 것도 중요하지만 이 함수를 C 언어에서 어떻게 호출하여 사용할 것인가 하는 문제도 중요하다. 흔히들 호출 규약(Calling Convention)이라고 표현하는 이것은 함수를 호출하는 규약인데, 몇가지 방식이 존재한다.

  • stdcall(pascal) 방식 : 스택에 파라메터를 역순으로 삽입하고 함수를 호출. 스택의 정리작업을 호출된 함수에서 수행. 파스칼 언어 및 베이직 언어에서 사용하는 방식
  • cdecl 방식 : 스택에 파라메터를 넣는 방식은 stdcall과 같음. 단 스택의 정리작업을 호출한 함수에서 수행. C언어에서 사용하는 방식
  • fastcall 방식 : 몇개의 파라메터는 레지스터를 통해 넘기고 나머지 파라메터는 스택을 사용하는 방식

 위의 세가지 중에서 보편적인 방식 두가지는 stdcall 및 cdecl 방식이다. 이 두가지 방식의 가장 큰 차이점은 스택의 정리를 누가 하는 가이다.

 stdcall 방식 같은 경우 Callee(호출 된 함수)에서 스택 정리를 하므로 Caller(호출하는 함수)와 Callee 모두 파라메터의 개수를 알고 있어야 정상적인 처리가 가능하다.

 반면 cdecl 방식 같은 경우 Caller에서 스택 정리를 하므로 Callee는 파라메터의 개수를 정확하게 몰라도 된다. 바로 이 점이 C 언어의 가변인자(Variable Argument)를 가능하게 하는 것이다(printf와 같은 함수를 생각해보자).

 가변인자에 대해서는 나중에 알아보고 우리가 사용할 cdecl에 대해서 자세히 알아보자.

 

2.2 cdecl 분석

 아래는 간단한 C 프로그램을 작성한 것이다.

  1. int DoSomething( int a, int b )
    {
        int c;
        c = a+b;
        return c;
    }
  2. int main(int argc, char* argv[])
    {
        DoSomething( 1, 2 );
  3. }

 

 간단하게 파라메터 2개를 받아서 그중 첫번째 파라메터를 리턴하는 함수이다. 이것을 cdecl로 해서 컴파일 한 결과 나온 어셈블리어 결과는 아래와 같다.

  1. int DoSomething( int a, int b )
    {
        int c;
        c = a+b;
        return c;
       /* 여기가 어셈블리어로 변경된 코드
        push ebp
        mov ebp,esp
        push ecx
        mov eax,[ebp+08h]
        add eax,[ebp+0Ch]
        mov [ebp-04h],eax
        mov eax,[ebp-04h]
        mov esp,ebp
        pop ebp
        retn
       */
    }
  2. int main(int argc, char* argv[])
    {
        DoSomething( 1, 2 );
       /* 여기가 어셈블리어로 변경된 코드
        push ebp
        mov ebp,esp
        push 00000002h
        push 00000001h
        call SUB_L00401000
        add esp,00000008h <== 스택을 정리하는 부분
        pop ebp
        retn
       */
    }


 위에서 보면 파라메터를 역순으로 Push 하는것을 알 수 있으며 main 함수에서 "add esp,08" 명령을 통해 스택 정리를 수행함을 볼 수 있다. 여기서 주의해서 봐야 할 부분은 DoSomething 함수에서 어떻게 파라메터에 접근하고 또한 어떻게 함수 내부적으로 사용하는 레지스터를 관리하고 복원하는가 이다.

 아래는 Caller(main)Callee(DoSomething)의 스택의 상태를 표시한 것이다.

 CallingConvension.PNG

<Caller와 Callee의 Stack>

 왜 ESP로 접근하지 않고 EBP를 통해 파라메터에 접근하는 것일까? 위의 그림을 보면 왜 ebp + Index로 접근을 하는 지 알 수 있다. 스택의 Top을 의미하는 ESP 레지스터의 경우 코드 중간 중간에 스택을 사용하면서 계속 변하는 값이다. 그 반면에 파라메터의 위치는 항상 고정적이므로 스택의 Top을 이용해서 파라메터에 접근하려면 문제가 발생한다. 따라서 EBP에 ESP의 값을 처음 설정해 두고 EBP를 이용해서 고정된 Offset으로 접근하는 것이다.

 이와 같이 하면 스택의 Top이 계속 바뀌더라도 EBP가 초기의 스택 Top의 위치를 가지고 있으므로 EBP + 8, EBP+ 12과 같은 값으로 접근 가능하다. 위에서 초기에 Callee의 Stack에서 Push ebp를 하고 난 뒤에 Stack의 Top은 esp1을 가르키고 있다. 이 값을 ebp에 넣게 되므로 ebp를 이용하면 Parameter에 고정된 Index( 8, 12, 16...)으로 접근을 할 수 있는 것이다.

 

2.3 stdcall

 stdcall의 경우에는 cdecl과 거의 차이가 없고 스택을 정리하는 부분만 차이가 있다.

  1. int DoSomething( int a, int b )
    {
        int c;
        c = a+b;
        return c;
       /* 여기가 어셈블리어로 변경된 코드
        push ebp
        mov ebp,esp
        push ecx
        mov eax,[ebp+08h]
        add eax,[ebp+0Ch]
        mov [ebp-04h],eax
        mov eax,[ebp-04h]
        mov esp,ebp
        pop ebp
        retn 08h <== 스택을 정리하는 부분
       */
    }
  2. int main(int argc, char* argv[])
    {
        DoSomething( 1, 2 );
       /* 여기가 어셈블리어로 변경된 코드
        push ebp
        mov ebp,esp
        push 00000002h
        push 00000001h
        call SUB_L00401000
        pop ebp
        retn
       */
    }

 

2.4 프롤로그(prologue) 및 에필로그(epilogue)

 Callee의 스택을 다시 Caller의 스택으로 복원해야 하는데 스택 top을 저장하고 복원하고 하는 작업을 프롤로그(prologue), 에필로그(epilogue)라고 한다. 위에서 본 스택을 복구하는 작업이다.

 만약 우리가 어셈블리어 함수를 만든다면? 그리고 그 함수를 C에서 호출한다면? 아니면 그 반대의 경우라면 어셈블리어 함수를 어떻게 만들어야 할까? 그렇다. 위에서 본 것과 같은 형태 즉 cdecl의 형태를 그대로 따라서 만들면 된다.

 DoSomething() 함수의 프롤로그 에필로그 형태는 아주 일반적인 형태이므로 알아두도록 하자.(꼭 저렇게 구성할 필요는 없지만 일반적이므로 알아두자.)

 

 

3.마치면서...

 자 오늘 우리는 어떻게 어셈블리어 함수를 만들어서 파라메터를 넘겨 받을 것이며, 어셈블리어 함수에서 C 함수를 어떻게 호출해야 하는지, 혹은 그 반대의 경우 어떻게해야 하는지에 대해서 알아보았다. 다음에는 좀 더 깊은 내용에 대해서 알아보자.

 

 

 

이 글은 스프링노트에서 작성되었습니다.

Part3. 인터럽트(Interrupt)

들어가기 전에...

0.시작하면서...

앞서 커널 개발시에 권장되는(??) 알아야하는 몇가지에 대해서 언급했었다.
그럼 이제 찬찬히 그것들에 대해서 알아볼텐데... 오늘은 인터럽트(Interrupt)에 대해서 한번 볼까 한다.

1.인터럽트(Interrupt)란?

1.1 인터럽트(Interrupt)의 정의

인터럽트(Interrupt)는 내부 or 외부에서 특정한 이벤트로 인해 실행중인 코드를 중단하고 해당 이벤트를 처리하는 예외적인 상황을 말한다. Intel Architecture에서는 이런 예외적인 상황을 아래와 같이 크게 2가지로 구분하고 있다.

  • 1.Interrupt : External(Hardware generated) Interrupt와 Software generated Interrupt를 포함.
  • 2.Exception : Processor-detected program-error exception과 Software-generated exception과 Machine-check exception을 포함. fault, traps, abort로 구분됨

뭔가 상당히 복잡한데, 위 2가지에 대한 자세한 내용은 여기서 설명하지 않을 것이고, 궁금한 사람은 Intel Architecture Manual의 Volume 2 : Software Developer's Manual을 살펴보기 바란다.
간단히 알아야 할 것만 설명하면, 저런 예외적인 상황은 Hardware 또는 Software 적인 방법으로 발생할 수 있고, 우리가 프로그램을 실행하면서 발생하는 Divide By Zero와 같은 에러도 Exception으로 처리가 된다는 사실이다.

편의상 위의 두가지 모두를 인터럽트(Interrupt)라고 부르기로 하자.

1.2 인터럽트(Interrupt)와 컨텍스트(Context)

내가 소시적에(??) OS를 만들면서 여러책을 보았다. 그때 한 OS 책에서 본 내용이 아직도 기억에 남는데... 그 책은 인터럽트에 대해서 이렇게 설명해 놓았다.

프로그램을 실행 도중, 인터럽트가 발생할 시 현재 작업을 중단하고 상태를 저장한 뒤 인터럽트 서비스 루틴(ISR)을 실행한다. 실행이 완료되면 중단된 시점부터 실행이 재개된다.

여기서 상태를 컨텍스트(Context)라고 이야기하는데, 컨텍스트는 실행중인 프로세스의 내용을 말한다. 위의 항목에서 내가 궁금했던 점은... "상태를 저장한 뒤" 라는 부분... 어느 부분을 어떻게 저장하는 것인지가 명확하지 않았다. 나중에 Intel 메뉴얼을 뒤져서 정확하게 알게 되었지만, 알고 난뒤의 허탈함 and 배신감이란 이로 말할 수 없었다.

과연 "상태"를 어디까지 저장해주는걸까? 아래는 Intel 메뉴얼에 Software Developer's Manual에 나오는 그림이다.

Intel Architecture의 Interrupt 발생시 Stack의 변화

벌써 눈치를 챈 사람들도 있겠지만... 그렇다. 스택(Stack)에 단순히 Flags/CS/EIP/Error Code가 저장되는 것이 전부였다. @0@)/~
그 말은 ISR에서 사용하는 레지스터들은 "알아서" 저장하고 사용한 뒤 "알아서" 복원해야 한다는 이야기다. 위에서 보면 Privilege Level이 변하지 않는 경우는 Flags/CS/EIP/Error Code를 저장하며, 변하는 경우는 SS/ESP를 추가로 더 저장한다. 추가로 저장된 SS/ESP는 인터럽트 처리가 끝났을 때 하위 레벨의 스택으로 다시 돌아가는 용도로 사용하는 것이다.

왜 추가로 더 저장하는가 하니... Intel Architecture는 총 4개의 Level( Ring0 ~ Ring3)을 가지고 Level간에 사용하는 스택을 다르게 설정할 수 있다. Level이 변경될 때 마다 CPU는 해당 Level의 Stack으로 변경해 주게되고 이것을 Stack Switching이라고 부른다(모든 레벨이 같은 스택을 사용할 수도 있다. 일단 Level이 다르면 Intel CPU는 SS/ESP를 Handler의 스택에 저장해 준다는 것만 알아두자.)

2.인터럽트의 처리

2.1 인터럽트 서비스 루틴(Interrupt Service Routine-ISR)의 컨텍스트(Context) 저장

커널을 만들려면 위에서 언급했던 인터럽트들을 처리해 주는 인터럽트 서비스 루틴(Interrupt Service Routine-ISR)을 만들어야 하는데, 보통 아래와 같은 형태를 띈다.

_kIsr :  
    **  pushad**
    **  push ds  
    push es  
    push fs  
    push gs  <== 여기까지가 컨텍스트(Context)를 저장하는 부분**

    ; 일단 커널 쪽에 진입해야 하므로 세그먼트 재 설정   
    mov ax, 0x10  
    mov es, ax  
    mov ds, ax
    mov fs, ax  
    mov gs, ax  

    **call HandlerFunction  <== Handler 함수 호출 부분 **  

    **  pop gs  
    pop fs   
    pop es  
    pop ds  
    popad  <== 여기까지가 컨텍스트(Context)를 복구하는 부분  

    iretd  <== 인터럽트 처리 완료후 복구하는 부분**

간단히 설명하면 위의 파란색 부분은 사용할 레지스터들은 저장하고 핸들러 루틴을 불러서 필요한 처리를 한다음 다시 복원하는 코드이다. 붉은 색 부분은 실제로 스택에 저장된 Flags/CS/EIPSS/ESP를 복원하고 코드로 돌아가는 코드이다.

크게 어려운 어셈블리어 명령이 아니기 때문에 설명은 다음으로 미룬다. @0@)/~ 궁금한 사람은 역시 메뉴얼을 참조하면 된다.

2.2 핸들러 루틴(Handler Routine)

지금까지 ISR의 컨텍스트 저장 코드에 대해 간단히 보았다. 이제 HandlerFunction이 무엇을 하며 어떻게 작성해야 하는지 한번 알아보자.

인터럽트가 발생하면 원인이 Interrupt인가 Exception인가에 따라서 HandlerFunction의 실제 역할이 다르고, Interrupt or Exception의 세부분류에 따라 그 역할이 다르다.

보통 Handler는 C 함수로 작성되고 우리가 흔히 알고 있는 일반 함수 코드 처럼 작성된다. 아래는 프레임워크에서 사용된 키보드 핸들러의 소스코드이다.

   //-----------------------------------------------------------------------------  
    //  
    // 키보드 핸들러 버퍼에 값을 집어 넣는다.  
    //  
    //-----------------------------------------------------------------------------  
    void kIsrKeyboard( void )  
    {  
        BYTE  bCh;
        bCh = kReadPortB( KBD\_PORT\_BUFFER );
        kPrintchxy\_low( GDT\_VAR\_VIDEOMEMDESC, 11, 0, bCh, 0x05 );
        // 키를 버퍼에 넣는다.  
        kAddKeyToBuffer( bCh );

        // Bottom Half 사용  
        g_stBottomHalfManager.vstBottomHalfUnit[ 1 ].bFlag = TRUE;  
        kSendMasterEoi();  
    }

우리가 흔히 쓰는 함수와 같은 형태를 하고 있으니 이해하는데는 문제가 없을 것이다. 코드를 보면 키보드 포트에서 값을 읽어서 버퍼에 저장하는 역할을 한다는 것을 금방 알 수 있다.

그렇다면 ISR 함수와 우리가 사용하는 일반 함수와 차이점이 무엇일까? 함수가 불리어지는 시점의 차이가 가장 큰 차이다.

ISR은 인터럽트가 발생한 시점에서 호출되는 코드이기 때문에 장시간 걸리는 작업을 실행하면 시스템 전체의 성능에 영향을 미치게 된다. 인터럽트가 발생하면 CPU에서 기본적으로 인터럽트를 불가 상태로 만든다음 ISR 함수를 호출하게 된다. 다시 말하면 ISR 처리 루틴이 완전히 끝나지 않는 한 다른 인터럽트가 발생하지 못한다는 것이다. 이것은 아주 치명적인데, 예를 들어 특정 인터럽트 루틴에서 무한루프를 돌면 키보드/마우스/타이머 등등이 먹통이된다. @0@)/~!!!!

2.3 주의해야 할 점

** ISR에서 절때 시간이 많이 걸리는 작업을 해서는 않된다.**

이것은 진리이며, 자칫 잘못하면 시스템 전체의 성능을 느리게 만든다. 예외 핸들링 시 레벨에 따라서 우선순위가 다르기 때문에 낮은 Level의 예외는 지연되게 되므로 더욱 세심한 배려가 필요하다. 최상위 Level의 핸들링에서 처리가 늦게되면 그 외에 낮은 Level의 예외는 당연히 지연될 수 밖에 없다.

인터럽트의 Level에 대한 분류는 가장 높은 것이 Hardware Reset과 Machine Checks이고 그 밑으로 이것 저것 있는데, 역시 궁금한 사람은 Intel Architecture 메뉴얼을 참조하도록 하자.

Intel Architecture의 예외 수준(Exception Level)

그리고 잊지 말아야 할 사실을 다시 한번 강조한다.

*인터럽터가 발생하면 기본적으로 ISR 핸들러 호출 시 EFLAG 레지스터의 인터럽터 가능 플래그가 0으로 되어 Disable된 상태라는 것이다. *

이 말은 인터럽터 핸들러 안에서 무한루프를 돌면, 그 외에 다른 인터럽터도 발생하지 않는다는 말과 동일하다. 따라서 각별한 주의가 필요하다.

3.인터럽트(Interrupt)의 처리흐름

인터럽트가 발생하면 수행중인 태스크는 중지되고 인터럽트 핸들러가 호출된다고 했다. 이것을 그림으로 표현하면 아래와 같다.

인터럽트 발생 시 흐름

특정 태스크가 실행중이다가 인터럽트가 발생하면 인터럽트 핸들러에서 인터럽트에 대한 처리가 되고, 그것이 완료된 다음에야 태스크로 복귀한다.

핸들러에서 작업이 늦게 끝나면 끝날수록 태스크로 복귀하는 시간이 늦어지며 이 시간 동안 거의 인터럽트가 불가능해서 전체적인 지연을 초래하는 것이다. 물론 요즘 커널들은 인터럽트 핸들러 안에서도 인터럽트가 발생할 수 있도록 하여 더욱 가용성을 높이고 있다.

인터럽트의 중복을 허용함으로써 오는 이득은 특정 인터럽트가 완료되지 않은 상태에서 다른 인터럽트의 처리가 가능하므로 많은 인터럽트를 다중으로 처리할 수 있다는 점이다. 대신 인터럽트가 다중으로 발생할 수 있으니 커널코드 자체도 재진입(reenterance) 가능하도록 해야 되니 커널 코드가 굉장히 복잡해 지는 단점을 가지고 있다.

커널 코드 내에서 인터럽트 발생이 가능하도록 한 좋은 예제로 Linux Kernel 2.6 버전이 있다. 커널이 2.6 버전으로 올라가면서 스케줄러 부분 및 메모리 관리 부분이 비약적으로 향상되었다는데, 궁금한 사람은 참고하는 것도 괜찮을 듯 하다.

5.마치면서...

인터럽트에 대한 위 내용 정도의 지식은 거의 필수라고 볼 수 있다. 이번 내용에서 깊은 내용은 다루지 않고 개론 정도만 언급했으므로 알아두도록 하자.

4.첨부

142147_part3.ppt
13.3 kB

+ Recent posts