나의 삽질 유산 답사기... #1. QEMU에서 0x00000000에 이미지를 올리고 싶었습니다.

나빌레라의 이미지

무슨 바람이 불었는지 삽질하다 말고 삽질 내용을 KLDP에 올리고 싶다는 생각이 들어서 키보드에 손가락을 올려 놓습니다. 별 내용은 아닌데 구글링하다가 답을 못 찾고 직접 삽질한 내용이라 기록을 남기는 의미에서라도 어딘가에 적어 놓고 싶었거든요. 그럴 공간이 제게 KLDP 말고 또 어디가 있겠습니까..

이 글을 시리즈로 올릴 생각은 없는데 그래도 혹시 모르니까 #1 이라고 번호를 붙였습니다. #2는 아마 또 쓸지 안쓸지 저도 모릅니다.

지난번에 제가 올렸던 글에서 밝혔던 대로 저는 요즘 취미 삼아 시간 나는 대로 RTOS를 하나 새로 만들고 있습니다. 타겟 보드는 보다 많은 사람들이 접근할 수 있도록 실물 보드를 쓰지 않고 QEMU를 사용합니다. 훌륭한 에뮬레이터이지요. 게다가 오픈 소스라서 어떤 문제가 생겼거나 원하는 동작을 이끌어 낼 방법을 찾지 못했을 때 마지막 수단으로 QEMU 자체의 소스 코드를 뜯어 볼 수 있어서 좋습니다. 이 글의 내용이 바로 그 QEMU 내부 코드 중 "아주 극히 매우" 일부를 분석해 본 내용입니다.

삽질하게 된 발단은 이러합니다. 대충 부트로더를 얼기설기 만드는 중이었습니다. 당연히 펌웨어 재배치하는 코드도 작업해야 하지요. 아~ 그런데 QEMU는 elf 파일을 읽으면 알아서 재배치를 해 주는 겁니다. 그래서 elf 파일에서 순수 바이너리만 추출해서 올려 보았더니 제 의도와는 다르게 0x00000000에 로딩을 하지 않는 겁니다. 제가 타겟으로 사용한 realview-pb-a8 에뮬레이팅에서는 0x10000에 바이너리를 로딩하는 겁니다. 아마도 리눅스 커널 이미지를 로딩하는 것을 전제로 만들어져서 그런가보다 짐작을 하고 이런 저런 옵션을 찾아 보았으나 로딩 위치를 바꾸는 옵션은 없었습니다.

며칠을 검색질로 어떻게 해 보려다가 결국 답을 못 찾고 QEMU 소스 코드를 뜯어보기로 결정했습니다. 이렇게 삽질은 시작되었습니다. 전 사실 QEMU의 소스 코드 구조를 모릅니다. 데이터 흐름이나 자료 구조의 연관 관계도 모릅니다. 그냥 약간의 짐작과 찍기 그리고 경험으로 미루어 짐작하는 느낌(결국 다 같은 말...)으로 분석을 시도한 것이죠.

QEMU는 소스 코드 분량이 만만치 않습니다. 1.7.1 버전 기준으로 파일이 6,200개라고 저 컴퓨터 파일 브라우저는 알려줍니다. 다 볼 순 없으니 필요한 파일을 찾아서 보아야 합니다. 저는 ARM 에뮬레이터에 보드는 realview-pb-a8을 선택했습니다. QEMU 디렉토리를 이리저리 다녀보니 hw/arm이라는 디렉토리가 보입니다. 이 디렉토리 밑에만 파일이 32개 있습니다.

[.]                [..]               armv7m.c           bcm2835_todo.c
boot.c             collie.c           exynos4210.c       exynos4_boards.c
gumstix.c          highbank.c         integratorcp.c     kzm.c
mainstone.c        Makefile.objs      musicpal.c         nseries.c
omap1.c            omap2.c            omap_sx1.c         palm.c
pxa2xx.c           pxa2xx_gpio.c      pxa2xx_pic.c       raspi.c
realview.c         spitz.c            stellaris.c        strongarm.c
strongarm.h        tosa.c             versatilepb.c      vexpress.c
xilinx_zynq.c      z2.c

파일 이름을 보니 각 파일들이 QEMU의 ARM 에뮬이 지원하는 보드에 대한 구현인것 같습니다. 그럼 이름으로 때려 맞춥니다. realview.c라는 파일이 보이는 군요! 일단 파일을 열어 봅니다.

오우! 파일이 길고 내용도 많습니다. 일단 이해할 생각은 안하고 그냥 쭉 봅니다. 소스 코드의 가장 아래 부분에 이런 내용이 있습니다.

static void realview_machine_init(void)
{
    qemu_register_machine(&realview_eb_machine);
    qemu_register_machine(&realview_eb_mpcore_machine);
    qemu_register_machine(&realview_pb_a8_machine);
    qemu_register_machine(&realview_pbx_a9_machine);
}
 
machine_init(realview_machine_init);

machine_init()은 매크로인데 정확히 뭘 하는 매크로인지는 모르겠으나 아무튼 이름에서 풍기는 냄새는 뭘 초기화 하는 것이고 파라메터를 추적해 보면 에뮬레이션 타겟 보드에 대한 추상화 정보를 가진 구조체를 넘기고 있습니다. 대충 감으로 때려 잡습니다. 아! 이거로 QEMU에 타겟 보드를 등록하는 거구나~

삽질할 당시엔 찾아보지도 않았는데, 이 글을 쓰면서 문득 궁금해져서 찾아봤습니다. machine_init()의 정체는 무엇일까.. 뭐 이러면서 하나 더 알고 가는 거지요~^^ QEMU 소스 코드 중에 include/qemu/module.h를 열어보면 그 정체가 있습니다.

#ifndef QEMU_MODULE_H
#define QEMU_MODULE_H
 
/* This should not be used directly.  Use block_init etc. instead.  */
#define module_init(function, type)                                         \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) {  \
    register_module_init(function, type);                                   \
}
 
typedef enum {
    MODULE_INIT_BLOCK,
    MODULE_INIT_MACHINE,
    MODULE_INIT_QAPI,
    MODULE_INIT_QOM,
    MODULE_INIT_MAX
} module_init_type;
 
#define block_init(function) module_init(function, MODULE_INIT_BLOCK)
#define machine_init(function) module_init(function, MODULE_INIT_MACHINE)
#define qapi_init(function) module_init(function, MODULE_INIT_QAPI)
#define type_init(function) module_init(function, MODULE_INIT_QOM)
 
void register_module_init(void (*fn)(void), module_init_type type);
 
void module_call_init(module_init_type type);
 
#endif

소스 파일 전체 내용이 길지 않아서 전부다 옮겼습니다. 보니까 module_init()이라는 매크로를 다시 부르고 module_init()이라는 매크로는 "do_qemu_init_"이라는 글자를 붙여서 __attibute__((constructor)) 속성을 가진 함수를 선언하는 군요. 그래서 다시 realview.c 코드의 맨 아래 부분과 결합해서 확장하면

static void __attribute__((constructor)) do_qemu_init_realview_machine_init(void) {  
    register_module_init(do_qemu_init_realview_machine_init, MODULE_INIT_MACHINE);                                   
}

대충 이런 코드를 만들어 주는 겁니다. __atrribute__((constructor))가 뭔지 구글링 해 보니,

https://kldp.org/node/68257

오우! kldp 글이 제일 먼저 나오고 설명도 아주 잘 되어 있군요! 역시 KLDP!

아무튼 실제 삽질할 때는 저렇게 안 찾아보고 그냥 아! init구나~ 하고 빠르게 넘어갔었습니다. 시간은 금이니까요. 보면 익숙한 구조체 이름이 보입니다. realview_pb_a8_machine이라는 이름이네요. 어떻게 생겼나 봅니다.

static QEMUMachine realview_pb_a8_machine = {
    .name = "realview-pb-a8",
    .desc = "ARM RealView Platform Baseboard for Cortex-A8",
    .init = realview_pb_a8_init,
};

역시 감으로 때려잡으면 .name 멤버 변수에 지정하는 이름이 QEMU 커맨드 명령에서 -M ? 했을 때 노출되는 이름이고 .desc에 지정하는 문자열이 그 설명으로 나오는가 봅니다. 그럼 문자열은 그러한가보다 하고 넘어가고 .init 멤버 변수에는 함수 포인터가 연결되어 있습니다.
realview_pb_a8_init() 함수를 보죠.

static void realview_pb_a8_init(QEMUMachineInitArgs *args)
{
    if (!args->cpu_model) {
        args->cpu_model = "cortex-a8";
    }
    realview_init(args, BOARD_PB_A8);
}

실제 소스 코드를 열어보면 알겠지만 다른 realview 계열 보드 초기화 함수도 구성이 비슷합니다. 결국엔 realview_init() 함수를 호출하고 파라메터만 A8이니 A9니 MPCORE니 하면서 변경하는 것이죠. 그럼 핵심은 realvew_init() 함수에 있습니다.

역시 realvew_init() 함수는 깁니다. 내용도 많습니다. 역시 다 분석할 필요 없습니다. 그냥 쭉 훑어 보는 거지요. 대충 보아하니 realvew-pb 보드에 달린 여러 주변 장치(peripheral device)를 구성해 주는 코드들입니다. 짐작은 했지만 QEMU의 소스 코드가 꽤나 구성이 깔끔하고 좋군요. 쭉~ 코드를 읽다보면 realview_init() 함수의 말미에 뭔가 느낌이 딱~! 오는 코드 몇 줄이 보입니다.

    realview_binfo.ram_size = ram_size;
    realview_binfo.kernel_filename = args->kernel_filename;
    realview_binfo.kernel_cmdline = args->kernel_cmdline;
    realview_binfo.initrd_filename = args->initrd_filename;
    realview_binfo.nb_cpus = smp_cpus;
    realview_binfo.board_id = realview_board_id[board_type];
    realview_binfo.loader_start = (board_type == BOARD_PB_A8 ? 0x70000000 : 0);
    arm_load_kernel(ARM_CPU(first_cpu), &realview_binfo);

냄새가 난다~ 냄새가 나~ 이쪽 코드에서 커널 이미지 관련 데이터를 설정하는 것 같습니다. 왜냐면 args->kernel_filename 뭐 이런 변수 이름이라든가 arm_load_kernel() 뭐 이런 함수 이름을 봐서 바로 느낌이 오지 않습니까! 그럼 arm_load_kernel() 함수로 가봅니다.

파일은 hw/arm/boot.c 입니다. 역시나 함수가 길군요. 늘 하던 대로 분석하거나 이해하려 하지 말고 코드를 느껴 봅니다. 코드 중간에 역시나 느낌이 쌔~ 한 코드가 보입니다.

    /* Assume that raw images are linux kernels, and ELF images are not.  */
    kernel_size = load_elf(info->kernel_filename, NULL, NULL, &elf_entry,
                           NULL, NULL, big_endian, ELF_MACHINE, 1);
    entry = elf_entry;
    if (kernel_size < 0) {
        kernel_size = load_uimage(info->kernel_filename, &entry, NULL,
                                  &is_linux);
    }
    if (kernel_size < 0) {
        entry = info->loader_start + KERNEL_LOAD_ADDR;
        kernel_size = load_image_targphys(info->kernel_filename, entry,
                                          info->ram_size - KERNEL_LOAD_ADDR);
        is_linux = 1;
    }
    if (kernel_size < 0) {
        fprintf(stderr, "qemu: could not load kernel '%s'\n",
                info->kernel_filename);
        exit(1);
    }
    info->entry = entry;
    if (is_linux) {
        :
      밑으로 코드 많음..

사실은 저 주석 보고 느낀겁니다. 주석도 코드의 일부잖아요. 주석 내용은 "Assume that raw images are linux kernels, and ELF images are not." 입니다. 오우! 영어. 개발자의 친구 영어입니다. 대충 해석해 보면 raw 이미지는 리눅스 커널로 간주하고 elf 이미지는 아니다. 로 해석됩니다.

그래서 elf를 올렸을 때 그냥 잘 되었던 거구나!

깨달음을 얻었습니다. 역시나 이유가 있었던 거였습니다. 이 코드만 봐서는 -kernel 옵션으로 들어온 파일이 진짜 리눅스 커널인지 판단하는데 load_elf()로 elf인지 한 번 검사하고 load_uimage()로 뭔지 또 검사한 다음에서야 리눅스 커널로 판단하는가 봅니다. 왜냐면 load_image_targphys() 함수에서 리턴이 0보다 작으면 더 진행하지 않고 "qemu: could not load kernel " 이라는 메시지를 뿌리면서 종료해 버리거든요. 그러면 최종적으로 load_image_targphys() 함수에서 리눅스 커널을 처리한다고 미루어 짐작할 수 있습니다.

그러면 load_uimage는 뭐지? 뭐 함수에 들어가 봅니다. hw/core/loader.c에 있네요.

int load_uimage(const char *filename, hwaddr *ep, hwaddr *loadaddr,
                int *is_linux)
{
    return load_uboot_image(filename, ep, loadaddr, is_linux, IH_TYPE_KERNEL);
}

아항! uboot 이미지구나! QEMU가 uboot 이미지를 별도로 받아서 처리하는지 처음 알았습니다. 삽질을 하다보니 이렇게 새롭게 아는 것도 생기는 군요. 그럼 뭐 여기까지 됐고...

엥!?!?! 그냥 일반 바이너리를 처리하는 코드가 없네... 위 코드 대로 흐름을 따라가면 elf도 아니고 uimage도 아닌 일반 바이너리는 무조건 리눅스로 커널로 간주해 버립니다. 뭐 그렇다 칩시다. 리눅스 커널도 결국 실행 코드를 가진 커다란 펌웨어 이미지라고 볼 수 있으니까.. 그런데 문제는 이미지가 로딩되는 주소를 고정해 버린다는 겁니다. 바로 이 코드에서요.

    if (kernel_size < 0) {
        entry = info->loader_start + KERNEL_LOAD_ADDR;
        kernel_size = load_image_targphys(info->kernel_filename, entry,
                                          info->ram_size - KERNEL_LOAD_ADDR);
        is_linux = 1;
    }

둘 째 줄에 있는 entry = info->loader_start + KERNEL_LOAD_ADDR; 이 코드가 문제입니다. info->loader_start는 앞서 realvew.c의 realvew_init() 함수의 아래 부분 코드 저 위에위에위에위에..... 인용한 코드에 보면

    realview_binfo.loader_start = (board_type == BOARD_PB_A8 ? 0x70000000 : 0);
    arm_load_kernel(ARM_CPU(first_cpu), &realview_binfo);

arm_load_kernel()을 부르기 전에 loader_start로 0x70000000을 입력합니다. 타겟 보드가 A8이라서 삼항 연산자의 true에 해당하는 값일 테지요. 그리고 KERNEL_LOAD_ADDR은 loader.c의 맨 위에 정의되어 있습니다.

#define KERNEL_LOAD_ADDR 0x00010000

그래서 elf도 아니고 uimage도 아닌 바이너리는 무조건 리눅스 커널로 간주하고 realview-pb-a8 일 경우에는 0x70010000 메모리에 이미지를 로딩합니다. 그리고 infoceter.arm.com에서 realview-pb-a8의 메뉴얼을 보면 메모리 맵에 이런 내용이 있습니다.

Dynamic memory mirror (0x70000000-0x7FFFFFFF)    0x00000000-0x0FFFFFFF

헐.. 즉, 0x70000000은 0x00000000에 미러 된다는 겁니다. 그래서 그냥 바이너리를 올리면 0x10000에 올라갔던 겁니다!!!!!
이유를 몰랐는데 이런 이유가 있을 줄이야!!!!!!

그럼 간단하게 저 KERNEL_LOAD_ADDR의 값은 0x0으로 바꿔 버리고 QEMU를 다시 빌드한 다음 돌리면 될겁니다. 하지만 이렇게 하면 저만 잘 됩니다. 제가 쓸 책을 보고 따라하는 사람들은 되질 않죠. 그 사람들도 모두 QEMU를 다시 빌드하면 모를까. 물론 책으로 출간 될지도 안될지도 모르지만 아무튼 그 책을 읽을지 안읽지 모르는 사람들에게 빡센 QEMU 빌드를 강요할 순 없지 않겠습니까? 그래서 저는 차선책을 생각해 내야만 했습니다. 만약 다른 방법이 전혀 없다면 뭐 QEMU 빌드하는 법까지 책에 써 넣어야 겠지요... 하지만 그러기는 너무 귀찮고 싫었습니다.

그래서 혹시나 하는 마음에 load_uboot_image() 함수를 보았습니다. 역시 같은 파일인 loader.c에 있군요. 적당한 길이의 함수 입니다. 나름 이해 하기도 어렵지 않군요. 코드를 읽다보니 희망이 보입니다. 제가 원하는 동작을 uimage로 구현할 수 있을 것 같습니다. 코드를 보면 중간에 이런 코드가 있습니다.

     case IH_TYPE_KERNEL:
        address = hdr->ih_load;
        if (loadaddr) {
            *loadaddr = hdr->ih_load;
        }

대충 분석해 보자면 헤더에 적힌 load 주소를 address에 할당한다는 겁니다. 만약 loadaddr이 지정되어 있더라도 헤더에 적힌 load 값으로 덮어서 결국 무조건 헤더에 적힌 load 주소를 사용한다는 코드입니다. 그리고 코드를 쭉 따라 내려가면 또 이런 코드도 보입니다.

rom_add_blob_fixed(filename, data, hdr->ih_size, address);

결국 최종적으로 address 변수에 적힌 주소에 이미지를 로딩하군요! 아~ 저의 소박한 바람... 0x00000000에 이미지를 로딩하고 싶다는 바람이 이루어 질 것 같습니다. elf에서 바이너리를 추출해서 uimage 헤더를 붙여 준 다음 uimage 헤더를 조작해서 로딩 주소를 0x70000000으로 설정하면 될것 같습니다. 다시 말씀 드리지만 realview-pb-a8은 0x70000000 주소가 0x00000000 주소로 미러링 되므로 0x70000000에 이미지를 올려야만 0x00000000에 내용이 들어가게 됩니다.

그래서 uboot 소스를 구해다가 uimage 헤더를 만드는 구조체를 긁어와서 헤더를 붙여주는 아주 간단한 프로그램을 만들었습니다. 전체 코드는 불필요한 부분이 많고 간단히 필요한 부분만 인용해보면

              uboot_image_header_t uimageHeader;
 
	uimageHeader.ih_magic = IH_MAGIC;
	uimageHeader.ih_size = 0;
	uimageHeader.ih_load = 0x70000000;
	uimageHeader.ih_ep =   0x0;
	uimageHeader.ih_os = 0;
	uimageHeader.ih_arch = IH_CPU_ARM;
	uimageHeader.ih_type = IH_TYPE_KERNEL;
	uimageHeader.ih_comp = IH_COMP_NONE;

이렇게 헤더의 얼개를 만들어 놓습니다. 여기서 핵심은 ih_load의 값이 0x70000000 이라는 거지요. 그리고 QEMU에서 해당 코드를 타기 위해서 ih_type을 IH_TYPE_KERNEL로 해 놓습니다. 관련 define은 uboot 코드에서 그대로 가져왔으니 궁금한 분은 찾아보시기 바랍니다. 그리고 size를 정확히 써 주기 위해서 흔한 파일 크기 얻는 코드를 작업했습니다.

	int sourcefd = open(argv[1], O_RDONLY | O_BINARY);
	int sourceSize = lseek(sourcefd, 0, SEEK_END);
 
	uimageHeader.ih_size = sourceSize;

lseek() 말고 한 번에 파일 크기 얻는 함수도 있었던것 같은데 저도 나름 늙은 개발자라 저렇게 옛날 방식으로 구하는 패턴 밖에 모릅니다.

아무튼 이렇게 해서 헤더 구조를 만들고 제가 넣고자 하는 실제 바이너리 이미지 앞에 헤더를 붙여서 QEMU에 던졌더니 그제서야 제가 원하는 동작을 하더라구요.

후아~ 글이 왜 이리 길어졌지...ㅎㅎㅎㅎ 그냥 옆 사람에게 얘기하듯 썼더니 더 길어졌나봅니다. 실제 작업은 저렇게 눈으로만 쫒지는 않았습니다. 정말로 해당 코드에 진입하는지 확인하기 위해서 진리의 printf() 신공을 사용하여 의심 가는 부분에 printf() 집어 넣어서 빌드하고 이미지 올려서 QEMU 실행하고.. 또 printf 넣고 빌드하고 실행하고.... 이걸 수십번 반복했습니다. 말 그대로 삽질이지요. 삽질.

그래서 이 글이 삽질기인겁니다~^^

elf가 아니라 바이너리 이미지를 0x00000000에 올려보고 싶었습니다. 단지 올려보고 싶었을 뿐입니다... 왠지 소설 은전 한 닢이 생각나는 오후 입니다.

댓글

jhumwhale의 이미지

포크레인이 아닌 빈약한 삽(printf)으로

적당한 포인트를 까내어

거대한 금덩어리를 발견하는 어드벤쳐 미스테리 액션 소설이라고 느끼면 비정상이려나요~.

재미있었습니다. :)

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.