01 따라하는 PSP 홈브루 개발 - Hello World 출력

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

 

들어가기 전에...

 

0.시작하면서...

 이 글은 PSP 홈브루(Homebrew)를 Step by Step 형식으로 개발할 수 있도록 도와주는 문서이다. PSP SDK를 사용하는 방법만 단순히 설명하기보다 PSP의 전체적인 동작 방식과 흐름과 함께 설명하는데 목적을 두고 있다. 이번 회에서는 Hello World를 출력하는  간단한 홈브루를 구현해 볼 것이며, 더불이 PSP Kernel과 PSP 응용프로그램의 연결고리도 함께 살펴볼 것이다.

 

1.예제 소스코드

 아래는 처음으로 작성하는 "Hello World" 프로그램이다. 아래의 코드를 DevkitPSP로 컴파일 및 링크하면 EBOOT.PBP 파일을 얻을 수 있다. 이 파일을 메모리스틱에 PSP->GAME->TEST 폴더로 복사하면 PSP Home Menu의 Game 항목에서 볼 수 있다.

  1. #include <pspkernel.h>
    #include <pspdebug.h>

    // 매크로 정의
    #define TRUE 1
    #define FALSE 0
  2. #define printf pspDebugScreenPrintf

    /* Define the module info section */
    PSP_MODULE_INFO("KKAMAGUI", 0, 1, 1);

    /* Define the main thread's attribute value (optional) */
    PSP_MAIN_THREAD_ATTR(THREAD_ATTR_USER | THREAD_ATTR_VFPU);

    // 종료 Flag
    int g_bExit = FALSE;

    int ExitCallBack(int arg1, int arg2, void *common)
    {
        printf( "%X %X", arg1, arg2 );
        g_bExit = TRUE;
        return 0;
    }
     
    int main(int argc, char *argv[])
    {
        int cbid;

        pspDebugScreenInit();

        // Call Back 설정
        cbid = sceKernelCreateCallback("Exit Callback", ExitCallBack, NULL);
        sceKernelRegisterExitCallback(cbid);
       
        printf("Hello World\n");

        while( g_bExit == FALSE )
        {
            ;
        }
        sceKernelExitGame();
        return 0;
    }

 

2.예제 소스코드 분석

 그럼 지금부터 위의 소스를 하나하나 분석해보자. 우선 가장 처음에 나오는 PSP_MOUDLE_INFO() 매크로는 pspmoduleinfo.h 파일에 정의되어있으며, 아래와 같이 특수한 섹션(.lib.ent.XX, .lib.stub.XX)에 데이터를 삽입하도록 되어있다. 주석을 보면 Source 파일에 반드시 있어야 한다고하니 필수 요소인 듯 하며, 나중에 PSP Kernel이 Load할 때 참고하는 정보임을 추측할 수 있다. 그냥 필수적으로 타이핑해야 하는 부분이라고 생각하면 된다.

 
  1. // pspmoduleinfo.h
  2. /* Declare a module.  This must be specified in the source of a library or executable. */
    #define PSP_MODULE_INFO(name, attributes, major_version, minor_version) \
        __asm__ (                                                       \
        "    .set push\n"                                               \
        "    .section .lib.ent.top, \"a\", @progbits\n"                 \
        "    .align 2\n"                                                \
        "    .word 0\n"                                                 \
        "__lib_ent_top:\n"                                              \
        "    .section .lib.ent.btm, \"a\", @progbits\n"                 \
        "    .align 2\n"                                                \
        "__lib_ent_bottom:\n"                                           \
        "    .word 0\n"                                                 \
        "    .section .lib.stub.top, \"a\", @progbits\n"                \
        "    .align 2\n"                                                \
        "    .word 0\n"                                                 \
        "__lib_stub_top:\n"                                             \
        "    .section .lib.stub.btm, \"a\", @progbits\n"                \
        "    .align 2\n"                                                \
        "__lib_stub_bottom:\n"                                          \
        "    .word 0\n"                                                 \
        "    .set pop\n"                                                \
        "    .text\n"                                                    \
        );                                                              \
        extern char __lib_ent_top[], __lib_ent_bottom[];                \
        extern char __lib_stub_top[], __lib_stub_bottom[];              \
        SceModuleInfo module_info                                       \
            __attribute__((section(".rodata.sceModuleInfo"),        \
                       aligned(16), unused)) = {                \
          attributes, { minor_version, major_version }, name, 0, _gp,  \
          __lib_ent_top, __lib_ent_bottom,                              \
          __lib_stub_top, __lib_stub_bottom                             \
        }

 

 그 다음에 오는 PSP_MAIN_THREAD_ATTR() 매크로는 pspmoduleinfo.h에 정의되어있으며 변수에 값을 설정하는 간단한 역할만 한다. Optional한 부분이므로 굳이 쓰지 않아도 되나, 만약의 사태를 대비해서 일단 추가해 두자.

  1. // pspmoduleinfo.h 파일에 포함된 내용
  2. /* Define the main thread's attributes. */
    #define PSP_MAIN_THREAD_ATTR(attr) \
        unsigned int sce_newlib_attribute = (attr)

 Thread의 Attribute는 pspthreadman.h 파일에 있으며 아래와 같이 정의되어있다. PspThreadAttributes에 나와있는 주석을 보면 Thread의 종류에따라 Device에 접근할 수 있는 범위가 제한됨을 알 수 있다. 아직은 USB나 WLAN을 접근할 일이 없지만, 추후에 접근할 일이 생기면 Thread의 목적에 맞게 Type을 설정해야 하는 듯 하다. 이런 것도 있다는 것만 알고 넘어가자.

  1. /** Attribute for threads. */
    enum PspThreadAttributes
    {
        /** Enable VFPU access for the thread. */
        PSP_THREAD_ATTR_VFPU = 0x00004000,
        /** Start the thread in user mode (done automatically
          if the thread creating it is in user mode). */
        PSP_THREAD_ATTR_USER = 0x80000000,
        /** Thread is part of the USB/WLAN API. */
        PSP_THREAD_ATTR_USBWLAN = 0xa0000000,
        /** Thread is part of the VSH API. */
        PSP_THREAD_ATTR_VSH = 0xc0000000,
        /** Allow using scratchpad memory for a thread, NOT USABLE ON V1.0 */
        PSP_THREAD_ATTR_SCRATCH_SRAM = 0x00008000,
        /** Disables filling the stack with 0xFF on creation */
        PSP_THREAD_ATTR_NO_FILLSTACK = 0x00100000,
        /** Clear the stack when the thread is deleted */
        PSP_THREAD_ATTR_CLEAR_STACK = 0x00200000,
    };

 

다음은 ExitCallBack() 함수인데, 이 부분은 뒤에 sceKernelRegisterExitCallback() 함수를 설명하면서 같이 진행하기로 하고 main() 함수를 먼저 보자. main() 함수에서 제일 먼저 우리를 반기는 것은 pspDebugScreenInit() 함수다.  pspDebugScreenInit() 함수는 과연 무엇을 하는 함수일까? 함수가 psp라는 Prefix로 시작하는 것을 보아 PSP Library 함수임을 알 수 있으며 실제 함수의 구현부를 보면 아래와 같이 되어있다.

  1. // scr_printf.c 파일의 일부
  2. #define PSP_SCREEN_WIDTH 480
    #define PSP_SCREEN_HEIGHT 272
    #define PSP_LINE_SIZE 512
    #define PSP_PIXEL_FORMAT 3
  3. ... 생략 ...
  4. void pspDebugScreenInit()
    {
       X = Y = 0;
       /* Place vram in uncached memory */
       g_vram_base = (void *) (0x40000000 | (u32) sceGeEdramGetAddr());
       g_vram_offset = 0;
       sceDisplaySetMode(0, PSP_SCREEN_WIDTH, PSP_SCREEN_HEIGHT);
       sceDisplaySetFrameBuf((void *) g_vram_base, PSP_LINE_SIZE, PSP_PIXEL_FORMAT, 1);
       clear_screen(bg_col);
       init = 1;
    }

 코드에서 Display Mode를 408x272로 설정하고 Display할 데이터가 있는 주소 즉 Graphic Buffer를 FrameBuffer로 설정하는데, g_vram_base를 시작 주소로 설정하는 간단한 코드이다. PSP_LINE_SIZE는 확실치는 않지만 한 라인에 대응하는 Pixel의 수인 듯 하다. SCREEN_WIDTH가 480이고 PSP_LINE_SIZE가 512인 것으로 보아, 여유있게 잡았거나 Align하는 단위의 문제 때문에 좀 더 크게 잡은 것이 아닐까 추측한다. pspDebugScreenPrintf() 함수는 printf() 함수와 하는 역할이 거의 비슷하니 다음 함수로 넘어가겠다. 혹시 세부 구현이 궁금하다면 scr_printf.c 파일에 나와있으므로 관심있으면 참고하자.

 

 pspDebugScreenInit() 함수 아래를 자세히 보면 sceKernelXXX로 시작하는 함수를 볼 수 있다. 이 함수는 sceXXX Prefix로 시작되며, 이것은 PSP의 Kernel Service(Function)임을 나타낸다. sceXXX 로 시작하는 함수는 PSP Kernel로부터 Import 되는 함수로서, PSP Kernel이 프로그램을 로딩하면서 해당 영역에 실제 함수 주소를 연결해 주는 것 같다. 실제로 sceKernelCreateCallback() 함수의 정의를 따라가보면 구현은 없고 아래와 같은 Dummy Table만 볼 수 있다.

  1. // ThreadManForKernel.S 파일의 일부분
  2.     .set noreorder

    #include "pspimport.s"

    #ifdef F_ThreadManForKernel_0000
        IMPORT_START    "ThreadManForKernel",0x00010000
    #endif
    #ifdef F_ThreadManForKernel_0001
        IMPORT_FUNC    "ThreadManForKernel",0x0C106E53,sceKernelRegisterThreadEventHandler
    #endif
    #ifdef F_ThreadManForKernel_0002
        IMPORT_FUNC    "ThreadManForKernel",0x72F3C145,sceKernelReleaseThreadEventHandler
    #endif
    #ifdef F_ThreadManForKernel_0003
        IMPORT_FUNC    "ThreadManForKernel",0x369EEB6B,sceKernelReferThreadEventHandlerStatus
    #endif
    #ifdef F_ThreadManForKernel_0004
        IMPORT_FUNC    "ThreadManForKernel",0xE81CAF8F,sceKernelCreateCallback
    #endif
  3. ... 생략 ...

 IMPORT_FUNC는 pspimport.s 에 포함된 매크로로서 pspimport.s는 아래와 같이 어셈블리어 코드로 치환해주는 역할만 한다. 아래의 IMPORT_FUNC의 가운데 정도에 위치한 .ent 부분이 Loading 후에 실제로 Function Address로 치환되며, 이 위치의 Function Address를 사용하는 것으로 추측된다.


  1. .macro IMPORT_START module, flags_ver

        .set push
        .section .rodata.sceResident, "a"
        .word   0
    __stub_modulestr_\module:
        .asciz  "\module"
        .align  2

        .section .lib.stub, "a", @progbits
        .global __stub_module_\module
    __stub_module_\module:
        .word   __stub_modulestr_\module
        .word   \flags_ver
        .word   0x5
        .word   __executable_start
        .word   __executable_start

        .set pop
    .endm

    .macro IMPORT_FUNC module, funcid, funcname

        .set push
        .set noreorder

        .extern __stub_module_\module
        .section .sceStub.text, "ax", @progbits
        .globl  \funcname
        .type   \funcname, @function
        .ent    \funcname, 0
    \funcname:
        .word   __stub_module_\module
        .word   \funcid
        .end    \funcname
        .size   \funcname, .-\funcname

        .section .rodata.sceNid, "a"
        .word   \funcid

        .set pop
    .endm

    .macro IMPORT_FUNC_WITH_ALIAS module, funcid, funcname, alias

        .set push
        .set noreorder

        .extern __stub_module_\module
        .section .sceStub.text, "ax", @progbits
        .globl  \alias
        .type   \alias, @function
    \alias:
        .globl  \funcname
        .type   \funcname, @function
        .ent    \funcname, 0
    \funcname:
        .word   __stub_module_\module
        .word   \funcid
        .end    \funcname
        .size   \funcname, .-\funcname

        .section .rodata.sceNid, "a"
        .word   \funcid

        .set pop
    .endm

 

 예제의 하부에 있는 sceKernelCreateCallback() 함수는 CallBack Function의 Name과 Function Address, 그리고 Parameter를 받아서 Calback ID를 생성하여 반환한다. 이 반환된 ID를 등록하면 Callback 함수가 동작하는데, 테스트 프로그램에서는 sceKernelRegisterExitCallback() 함수를 사용해서 PSP의 Home 버튼과 연결하였다. sceKernelRegisterExitCallback() 함수는 Program이 끝났을 때, 즉 PSP의 Home 버튼을 눌러서 "예"를 선택했을 때 Callback을 불러주도록 등록하는 역할을 한다.

 Callback 함수의 원형은 아래와 같이 총 3개의 파라메터를 받도록 되어있으며, ExitCallBack() 함수의 생김새와 동일함을 알 수 있다.

  1. // pspthreadman.h 파일의 일부
  2. /** Callback function prototype */
    typedef int (*SceKernelCallbackFunction)(int arg1, int arg2, void *arg);

 ExitCallBack() 함수는  sceKernelRegisterExitCallback()에 의해 등록되었으므로,PSP의 Home 버튼을 눌렀을 때 XMB로 돌아가기를 선택하는 경우 불려진다. 혹시 Home 버튼을 눌렀을때 뜨는 선택 화면에서 호출되는 것이 아닌지 궁금해 할지도 몰라서, 테스트 해보기 위해 아래의 printf() 기능을 넣었다. 만약 Home 버튼을 누른 후 표시되는 메뉴에서 CallBack이 호출된다면 "아니오"를 선택하고 빠져나왔을때 화면에 무엇인가 출력되었어야 한다. 하지만 아무런 값이 출력되지 않은 것을 보아 메뉴에서 "예"를 누른 후 호출되는 것을 알 수 있다.

  1. int ExitCallBack(int arg1, int arg2, void *common)
    {
        printf( "%X %X", arg1, arg2 );
        //g_bExit = TRUE;
        return 0;
    }

 

 마지막 함수는 sceKernelExitGame() 함수이다. sce Prefix가 붙어있으므로 PSP Kenrel Service 임을 알 수 있으며, 이 함수를 호출하면 다시 PSP 메뉴(XMB)로 이동하게 된다. 이 함수를 ExitCallback()에서 부르게 되면 충돌이 발생하여 메뉴로 돌아가지 못하고 멈추므로 주의해야 한다.

 

3.컴파일 및 실행

 DevkitPro가 설치된 폴더의 PSP Sample Code 폴더에 보면 elf_template 폴더가 있다(D:\devkitPro\devkitPSP\psp\sdk\samples\template\elf_template). 그 폴더의 main.c 파일에 예제 소스를 덮어쓰고 cmd.exe를 실행하여 make를 입력하면 EBOO 파일이 생성될 것이다. 이 파일을 PSP 메모리스틱에 H:\PSP\GAME\Test 폴더에 넣은 후, PSP 메뉴의 게임->Memory Stick에 가면 실행할 수 있다. 화면 우측 상단에 하얀색으로 빛나는 멋진 Hello World가 표시될 것이다. @0@)/~

 

 

4.마치면서...

 이것으로 Hello World를 화면에 출력하는 PSP 홈브루(Homebrew)를 완성하였다. 첫시간이니만큼 API의 사용법보다는 전체적인 소스의 구성 및 구조에 더 치중하려고 노력하였다. 다음은 PSP의 키입력 처리에 대해서 알아보도록 하자.

 

 

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

+ Recent posts