OS를 만듭시다. 어때요~ 참 쉽죠? (14)

나빌레라의 이미지

OS. 영어로 풀어서 Operating System. 한자어로 번역해서 운영체제. 이것을 만든다고 하면 사람들은 굉장히 어렵게 생각한다. 리눅스나 윈도우같은 OS를 비교대상으로 본다면 OS를 만드는것은 정말 어렵고 힘들고, 개인이 만들기엔 어쩌면 불가능에 가까운 도전일지도 모른다. 하지만 OS의 기본 개념들은 대학교 학부과정에서 가르칠 정도로 이미 보편화 되어 있고 그 개념 자체들은 그다지 어렵지 않다. 개념 구현을 중심으로 동작하는 것 자체에 의미를 둔 OS를 만드는 것은 어쩌면 도전 해 볼만 한 가치가 있는 시도가 아닐까.

목차


1회
2회
3회
4회
5회
6회
7회
8회
9회
10회
11회
12회
13회

14. 시스템콜

리눅스나 유닉스 윈도우 등 완성도 높은 대형 상용 운영체들은 기본적으로 시스템 자원을 사용자 프로세스가 함부로 접근하지 못하게 되어 있다. 메모리 주소영이 커널영역과 사용자 영역으로 명확히 구분되어 있어서 사용자 프로세스에서 커널영역에 접근을 시도 하게 되면 당장에 Segmentation Fault 메시지를 출력하면서 프로세스가 종료되어 버린다. 커널쓰레드 및 디바이스드라이버 계층에서만 커널영역 메모리 영역에 접근 할 수 있다. 즉, 특권을 가지고 있는 프로세스만 커널영역의 메모리주소에 접근 할 수 있다. 그래서 사용자프로세스가 시스템자원을 이용하려면 직접 시스템자원에 접근하는 것이 아니라 커널에게 해당 자원에 대한 서비스를 요청하여야 하는데, 그럴때 사용하는 일종의 커널API가 바로 시스템콜 이다. 우리가 아무 생각없이 사용하는 open(), read(), write(), close(), fork(), exit() 이런것들 다 시스템 콜이다. 리눅스에는 약 280개 정도의 시스템콜이 있는것으로 알고 있다.

시스템콜은 특권모드에서 동작하는 것이다. ARM 에서는 시스템콜을 위해서 SWI 명령이 아예 준비되어 있다. x86에서는 eax 에 시스템콜번호를 넣고 0x80번 인터럽트를 발생시킨다. ARM 이 활용성면에서는 훨씬 편하게 되어 있는 듯 하다. User Mode 의 테스크에서 SWI 명령을 사용해서 특권모드인 SVC모드로 진입을 하고, SWI 핸들러는 SWI 명령의 인자로 넘어오는 시스템콜번호를 보고서 어떤 동작을 할지를 결정하면 된다.

리눅스의 예를 들자면 1번 시스템콜은 exit() 이다. 2번 시스템콜은 fork(), 3번 시스템콜은 read(), 4번은 wrie(), 5번은 open(), 6번은 close() 이다. 이런식으로 각 시스템콜은 번호로 구분된다. fork()를 예를 들자면 실제 리눅스 커널에 선언되어 있는 fork() 역할을 하는 함수의 이름은 sys_fork() 이다. 하지만 우리는 fork() 를 호출해서 사용한다. fork() 는 리눅스 커널에 선언된 함수가 아니라 libc 표준라이브러리에 선언되어 있는 랩핑함수 이다. 랩핑함수에서는 매개변수를 세팅하고 SWI 0x02 정도의 간단한 내용이 들어가 있다. SWI 0x02 가 처리되는 시점에서 SVC모드로의 ARM 프로세스 동작 모드 변환이 일어나고 Software Interrupt 가 발생하고 Exception Vector Table 을 따라서 SWI 명령에 대한 핸들러로 진입하고, 그곳에는 보통 System Call Vector Table 이라는 것이 있다. fork() 의 시스템콜 번호가 0x02 이므로 0x02 * 4 하면 해당 시스템콜에 대한 핸들러 함수 주소를 가지고 있는 System Call Vector Table 상의 위치가 나올터이고 이곳에서야 비로소 커널 내부에 존재하는 sys_fork() 함수로 가는 브렌치 명령이 존재 한다. 그러면 이제 sys_fork() 로 진입해서 실질적인 fork() 작업을 커널이 수행하게 된다.

완전히 정확하진 않지만 리눅스는 대략 위와 같은 과정을 따라서 시스템콜을 처리 한다. 나빌눅스도 리눅스의 시스템콜 처리 프로세스를 최대한 비슷하게 흉내 내어 보려 한다. 대신 훨씬 쉽게 구현 해 볼 생각이다.

우선 어떤식으로 만들지를 개략적으로 생각 해 보자. 리눅스의 libc 에 해당하는 navilnux_lib 정도의 나빌눅스 자체의 표준라이브러리겸 시스템콜함수 랩퍼함수가 필요할듯 하다. 이 랩퍼함수는 아무것도 하는 일 없이 "SWI" 어셈블리 명령만 한번씩 해주는 역할 정도로 끝날듯 하다. 그리고는 Core_swiHandler 함수를 고쳐야 한다. 지금까지는 컨텍스트스위칭을 테스트하기 위한 코드가 Software Interrupt 핸들러에 작성되어 있었는데, 이제 Software Interrupt 핸들러에서 무조건적으로 컨텍스트 스위칭을 할 필요는 없다. 그러므로 해당 코드를 수정 해 주어야 한다. 그리고 나빌눅스 커널 영역에 실제 시스템콜 본체에 대한 코드가 들어가야 한다.

좀더 간략히 설명해 보자면,

 1. 사용자테스크함수에서 시스템콜호출 (좀더 정확히는 시스템콜랩퍼함수 호출)
 2. 시스템콜랩퍼 함수는 swi 명령으로 Software Interrupt 발생
 3. Software Interrupt 핸들러에서는 swi 명령의 인자로 넘어오는 시스템콜번호를 기준으로 해서 커널에 있는 System Call Vector Table 로 부터 실제 시스템콜함수의 주소를 얻어, 해당함수로 점프
 4. 커널의 시스템콜 함수 실행

위 네 단계의 과정을 거칠 것이다. 그럴려면 추가로 System Call Vector Table 에 해당하는 커널전역변수가 하나 추가 되어야 할것 같다. 그리고 테스트 삼아서 아무것도 안하고 매개변수 받아서 화면에 출력하는 시스템콜 하나를 추가해보자. 파일명은 navilnux_sys.c 이다.

#include <navilnux.h>
 
unsigned int navilnux_syscallvec[SYSCALLNUM];
 
int sys_mysyscall(int a, int b, int c)
{
    printf("My Systemcall - %d , %d , %d\n", a, b, c);
    return 333;
}
 
 
 
 
void syscall_init(void)
{
    navilnux_syscallvec[SYS_MYSYSCALL] = (unsigned int)sys_mysyscall; 
}

navilnux_syscallvec[] 배열이 바로 나빌눅스의 System Call Vector Table 이 되겠다. 별거 아니다. 각 시스템콜 함수의 함수포인터 주소를 가지고 있는 그냥 평범한 일차원 배열이다. 그리고 sys_mysyscall() 함수가 나빌눅스에 최초로 추가된 시스템콜 되시겠다. 하는일은 3개의 매개변수를 그대로 화면에 출력해주고 333 이라는 숫자를 리턴한다. 시스템콜관련 레이어가 커널에 추가되었으니 이를 초기화 하는 함수역시 당연히 필요하다. 그 역할을 하는 함수가 syscall_init()함수이다. 이 함수가 하는 일은 말 그대로 System Call Vector Table을 초기화하는 일이다. 초기화란 벡터테이블에 실제 함수포인터를 연결해 주는 역할을 한다. syscall_init()함수는 navilnux_init() 함수에서 호출 된다.

커널시스템콜에 대한 프로토타입정보를 가지고 있는 헤더파일은 두개가 필요하다. navilnux_sys.h 와 syscalltbl.h 이다.

먼저 navilnux_sys.h 이다.

#ifndef _NAVIL_SYS
#define _NAVIL_SYS
 
#include <syscalltbl.h>
 
#define SYSCALLNUM 255
 
void syscall_init(void);
 
 
int sys_mysyscall(int, int, int);
 
#endif

내부에서 syscalltbl.h 를 include 하는 것을 볼 수 있다. navilnux_sys.h 파일에 커널시스템콜 관련 함수들의 프로토타입이 선언되어 있다. SYSCALLNUM 을 255로 지정해 놓았으므로, 나빌눅스는 255개 까지의 시스템콜을 사용 할 수 있다. 아래 쪽에는 navilnux_sys.c 에 있는 함수들의 프로토타입이 들어간다.

이어서 syscalltbl.h 를 보자

#ifndef _NAVIL_SYS_TBL
#define _NAVIL_SYS_TBL
 
#define SYS_MYSYSCALL   0
 
#endif

정말 별 내용없다. 그냥 define 문만 있다. 시스템콜이 추가 될 때 마다 이 파일에 define 문을 하나씩 추가해서 해당 번호로 연결되는 시스템콜을 만들어 가는 것이다. deifine 문만 존재 하는 파일이기 때문에 navilnux_sys.h 에 같이 있어도 상관없는 내용이긴 하지만 나중에 어셈블리 소스파일에서도 따로 include 하기 때문에 파일을 분리 해 놓았다.

다음은 방금 추가한 시스템콜을 실질적으로 호출하는 사용자테스크함수의 내용을 보겠다. 코드 자체는 별것 없다. 시스템콜을 추가하는데 있어서 중요한것은 호출단계에 따라서 어떤 값이 어떤식으로 전달받고 반환받는지에 대한 이해이다.

                        :
                        :
void user_task_3(void)
{
    int a, b, c;
 
    a = 1;
    b = 2;
    c = a + b;
 
    while(1){
        printf("TASK3 - a:%p\tb:%p\tc:%p\n", &a, &b, &c);
        c = mysyscall(1,2,3);
        printf("Syscall return value is %d\n", c);
        msleep(1000);
    }
}
                         :
                         :

navilnux_user.c 의 내용중 나머지 부분은 수정되지 않았고, user_task_3() 함수의 while 루프 안에서 mysyscall() 이라는 함수에 1,2,3을 매개변수로 넘겨주어 그 반환값을 출력하는 두 줄이 추가 되었다. mysyscall() 함수가 sys_mysyscall() 함수를 swi 명령을 통해서 호출해주는 시스템콜 랩퍼 함수이다.

리눅스도 같은 원리로, fork() 를 호출 할 경우 우리는 그냥 프로그램 코드에서 fork()를 호출하지만, 실제로는 libc 에 있는 fork() 함수에서 Software Interrupt 를 발생시키고, Software Interrupt 핸들러는 System Call Vector Table 로 연결되고, fork()에 해당하는 2번 벡터에는 sys_fork() 라는 커널함수의 주소가 등록되어 있어서 실제로 수행되는 함수는 sys_fork() 가 되는 식이다. 나빌눅스도 마찬가지 과정을 따른다.

그렇다면 나빌눅스도 시스템콜 랩퍼 함수가 필요하다. 일종의 랩퍼함수 라이브러리 같은 형식으로 커널에서 제공하게 될 것이므로 파일명은 navilnux_lib.S 로 하자. 왜 C파일이 아니라 S파일인가. 별 이유는 없다. 내용이 어셈블리어로 작성되어 있기 때문이다. 내용은 별거 없다.

#include <syscalltbl.h>
 
.global mysyscall
mysyscall:
    swi SYS_MYSYSCALL 
    mov pc, lr

정말 없어도 너무 없다 싶을 정도로 내용이 없다. 위에 별도로 syscalltbl.h 를 따로 선언한 이유가 바로 이 어셈블리 파일에서 include 하기 위함이다. 어셈블리 파일인데 여기에 C 언어 함수 프로토타입등이 있으면 컴파일할때 에러가 발생 하기 때문에, 시스템콜 번호만 따로 define 해 놓은 헤더파일을 따로 만들어 놓은 것이다. SYS_MYSYSCALL 은 위에 0 번으로 define 되어 있으므로 위 어셈블리 문장은 swi 0x00 을 실행한것과 동일하다. 그러면 swi 를 decode 하는 과정에서 Software Interrupt 핸들러로 들어갔다 나오고 그러고 나면 lr 을 pc 로 mov 한다. 즉, 원래 호출했던 호출자(여기서는 사용자테스크인 user_task_3()함수) 로 되돌아 가게 된다.

어셈블리어의 레이블인 mysyscall 을 함수로 호출하기 위해서 헤더파일을 추가 한다. 헤더파일 이름은 당연히 navilnux_lib.h 이다.

#ifndef _NAVIL_LIB
#define _NAVIL_LIB
 
extern int mysyscall(int, int, int);
 
#endif


여기까지는 별다를것 없는 평이한 내용이다. 그냥 시스템콜의 레이어를 따라서 저러한 코드들을 추가 하는 것이다. 이제 정말 중요한 것은 이와 같은 레이어를 직접 처리하는 swi 핸들러를 작성하는 것이다. entry.S 의 Core_swiHandler 레이블의 코드를 수정한다. 일단 먼저 완성된 코드를 보고 설명을 하겠다.

.global Core_swiHandler
Core_swiHandler:
    ldr     sp, =svc_stack
 
    stmfd   sp!, {lr}
    stmfd   sp!, {r1-r14}^
    mrs     r10, spsr
    stmfd   sp!, {r10}
 
    ldr     r10, [lr,#-4]
    bic     r10, r10, #0xff000000
    mov     r11, #4
    mul     r10, r10, r11
 
    ldr     r11, =navilnux_syscallvec
    add     r11, r11, r10
    ldr     r11, [r11]
    mov     lr, pc
    mov     pc, r11
 
    ldmfd   sp!, {r1}
    msr     spsr_cxsf, r1
    ldmfd   sp!, {r1-r14}^
    ldmfd   sp!, {pc}^

코드 자체는 그다지 길지 않다. 하지만 중요한 내용이기 때문에 부분부분 잘라서 부분코드로 설명을 하겠다. 코드 자체가 gcc 에 상당히 종속적으로 작성되었기 때문에 조심히 생각 해 보아야 할 코드이다. 컴파일러가 바뀌거나 gcc 버전에 따라 처리 방식이 다르다면 위의 코드는 수정되어야 함을 염두에 두어야 한다.

    ldr     sp, =svc_stack
 
    stmfd   sp!, {lr}
    stmfd   sp!, {r1-r14}^
    mrs     r10, spsr
    stmfd   sp!, {r10}

먼저 sp 에 svc_stack 의 값을 넣는다. svc_stack 은 맨처음 초기화 할때 svc 모드에 할당하는 스택의 초기값인 0xa0300000 번지이다. 그 다음으로는 svc 모드의 lr 을 스택에 백업한다. 앞서 강좌에서 여러번 설명했듯이 ARM은 Exception Mode를 전환 할 때 자동으로 pc-4 를 해서 전환되는 Exception Mode 의 lr 에 값을 넣어준다. 그러므로 lr 에는 User 모드로 돌아갈때 가야 할 주소가 들어가 있다. 이어서 r1~r14 까지의 User Mode 레지스터를 백업 한다. 이어서 spsr 을 백업 한다. 역시 앞서 설명이 있었는데, ARM 은 Exception Mode 를 전환 할 때 현재모드의 cpsr 을 전환될 모드의 spsr 에 복사한다. 즉 여기서 spsr을 스택에 백업하는 것은 User Mode 의 cpsr 을 복사하는 것과 같다.

독자들은 위 코드를 보고 무언가 의문을 품어야 한다. 꼭 그래야 한다. 의문을 품어 보아라. 생각 해 보아라. 생각 할 시간을 10초간 주겠다. :)

:
:
:

생각 해 보았는가? 그렇다 왜 r0~r14 가 아니라 r1~r14 인가. 왜 r0 는 백업하지 않는가. 그것은 gcc 가 리턴값을 처리하는 방법에 원인이 있다. 결론 부터 얘기 하자면 gcc 는 리턴값을 r0 에 넣어서 함수를 끝낸다. 아래에 예제 코드를 보도록 하자. 위에서 나왔던 sys_mysyscall() 함수의 C 언어 코드와 이를 역어셈블한 코드이다.

int sys_mysyscall(int a, int b, int c)
{
    printf("My Systemcall - %d , %d , %d\n", a, b, c);
    return 333;
}

a000bccc <sys_mysyscall>:
a000bccc:	e1a0c00d 	mov	ip, sp
a000bcd0:	e92dd800 	stmdb	sp!, {fp, ip, lr, pc}
a000bcd4:	e24cb004 	sub	fp, ip, #4	; 0x4
a000bcd8:	e24dd00c 	sub	sp, sp, #12	; 0xc
a000bcdc:	e50b0010 	str	r0, [fp, -#16]
a000bce0:	e50b1014 	str	r1, [fp, -#20]
a000bce4:	e50b2018 	str	r2, [fp, -#24]
a000bce8:	e59f001c 	ldr	r0, [pc, #28]	; a000bd0c <sys_mysyscall+0x40>
a000bcec:	e51b1010 	ldr	r1, [fp, -#16]
a000bcf0:	e51b2014 	ldr	r2, [fp, -#20]
a000bcf4:	e51b3018 	ldr	r3, [fp, -#24]
a000bcf8:	ebfff627 	bl	a000959c <printf>
a000bcfc:	e3a03f53 	mov	r3, #332	; 0x14c
a000bd00:	e2833001 	add	r3, r3, #1	; 0x1
a000bd04:	e1a00003 	mov	r0, r3
a000bd08:	e91ba800 	ldmdb	fp, {fp, sp, pc}
a000bd0c:	a000c050 	andge	ip, r0, r0, asr r0

우리가 관심을 가져야 하는 것은 리턴값인 333 을 어떻게 처리하느냐 하는 것이다. printf() 함수를 호출한 직후에 바로 return 333 하였으므로, 역어셈블한 코드에서도 해당 부분만 찾아보면 될 것이다. 그부분은 바로 아래와 같다.

a000bcfc:	e3a03f53 	mov	r3, #332	; 0x14c
a000bd00:	e2833001 	add	r3, r3, #1	; 0x1
a000bd04:	e1a00003 	mov	r0, r3

332 를 r3 에 넣고 거기에 1을 더해서 333을 만든다. 왜 저렇게 하는지는 모르겠지만 gcc 가 저렇게 만들었으니 필자는 할말이 없다. 저렇게 해야만 하는 이유가 있는가 보다. 그리고 중요한것은 그렇게 한다음 r0 에 r3을 복사한다음, 함수를 끝낸다는 것이다. 즉 r0 에 리턴값이 들어간다. 그럼 함수를 호출 하는 쪽에서도 당연히 r0 를 사용해야 할것이다. 이를 확인 해 보자.

void user_task_3(void)
{
    int a, b, c;
 
    a = 1;
    b = 2;
    c = a + b;
 
    while(1){
        printf("TASK3 - a:%p\tb:%p\tc:%p\n", &a, &b, &c);
        c = mysyscall(1,2,3);
        printf("Syscall return value is %d\n", c);
        msleep(1000);
    }
}

역시 위에서 보았던 user_task_3() 함수의 내용이다. mysyscall() 함수를 호출하고 그 반환값을 c 에 받아서 이를 printf() 의 인자로 넘기는 코드가 있다. 이 코드들이 컴파일되면 어떻게 어셈블리어로 바뀌는지를 보자.

           :
           :
a000beb8:	ebffffab 	bl	a000bd6c <mysyscall>
a000bebc:	e1a03000 	mov	r3, r0
a000bec0:	e50b3018 	str	r3, [fp, -#24]
a000bec4:	e59f0014 	ldr	r0, [pc, #20]	; a000bee0 <user_task_3+0x80>
a000bec8:	e51b1018 	ldr	r1, [fp, -#24]
a000becc:	ebfff5b2 	bl	a000959c <printf>
           :
           :

역어셈블 한 코드가 상당히 길어서 필요한 부분만 표기 하였다. 위 코드를 보면 mysyscall 함수를 bl 로 호출했다가 돌아오고 나면 r0 를 r3 에 복사한다음, 이를 스택에 넣고 스택으로 부터 printf() 에 들어갈 두번째 인자 (첫번째 인자는 "Syscall return value is %d\n" 이라는 문자열 자체 이다.) 로 사용함을 알 수 있다.

하나의 예 밖에 들지 않았지만 궁금한 독자들은 간단하게 값을 리턴하고 이 값을 받아서 무슨 작업을 하는 프로그램을 만든 다음 컴파일 해보고 arm-linux-objdump 명령을 이용해서 역어셈블 해서 확인 해 보면 알 것이다. arm-linux-gcc 는 r0를 이용해서 리턴값을 호출자로 넘긴다.

다시 Core_swiHandler 의 나머지 부분에 대한 설명으로 넘어가면,

    ldr     r10, [lr,#-4]
    bic     r10, r10, #0xff000000
    mov     r11, #4
    mul     r10, r10, r11
 
    ldr     r11, =navilnux_syscallvec
    add     r11, r11, r10
    ldr     r11, [r11]
    mov     lr, pc
    mov     pc, r11

위 코드에서 앞에 나오는 두줄은 swi 명령으로 부터 시스템콜 번호를 분리하는 코드이고 다음 두줄은 그렇게 해서 분리된 시스템콜 번호에 4를 곱하는 코드이다. 왜 4를 곱하냐면, 실제 호출되어야 할 시스템콜 함수들은 navilnux_syscallvec 이라는 커널 전역 배열 변수에 함수포인터 들이 들어가 있기 때문에 시스템콜 번호가 1번이면 navilnux_syscallvec 의 시작 주소로 부터 4바이트 떨어진 곳의 주소를, 시스템콜 번호가 2번이면 navilnux_syscallvec 의 시작 주소로 부터 8바이트 떨어진 곳의 주소를 가져와야 하기 때문이다. 즉 시스템콜번호*4 만큼 떨어진 곳의 주소를 가져와야 하기 때문에 swi 명령으로 부터 추출한 시스템콜 번호로 부터 4를 곱하는 코드가 들어간다.

그리고 이어서 navilnux_syscallvec 의 배열 주소를 가져 오고 여기에 아까 4를 곱한 offset 의 값을 더한다. 즉 navilnux_syscallvec 의 주소가 base 가 되고 시스템콜번호*4 한 값이 offset 이 되는 것이다. 이 base+offset 의 값이 r11 에 들어가고 이 r11의 실제 값을 다시 r11 에 읽은다음, 현재의 pc 를 lr 에 복사하고, r11을 pc 에 복사한다. 즉, 해당 함수로 점프 한다.

해당 함수에 들어갔다 나오면서 해당 함수의 리턴값은 r0 에 저장이 된다. 이 r0 를 유지 한채로 컨택스트 백업 작업을 해야 한다. 그렇기 때문에 위에서 r0~r14 까지 컨택스트 백업을 한것이 아니라, r1~r14까지 컨택스트 백업을 한 것이다. r0 는 애초부터 컨택스트 백업 작업에서 제외 되어 있었기 때문에 navilnux_syscallvec 으로 부터 얻어진 시스템콜 함수에 들어갔다 나오면서 리턴값에 의해 r0 가 수정되더라도 그 변화된 값이 그대로 사용자테스크 함수 까지 전달 될 수 있는 것이다.

그래서 Core_swiHandler 의 나머지 코드는 컨택스트를 복구 하는 평이한 코드이다.

    ldmfd   sp!, {r1}
    msr     spsr_cxsf, r1
    ldmfd   sp!, {r1-r14}^
    ldmfd   sp!, {pc}^

여기 까지가 실질적인 코드작업은 끝이다. 하지만 한가지 설명을 하지 않은 것이 있다. 눈치빠른 독자라면 이미 눈치 챘을 것이다. 바로 매개변수에 대한 설명이 빠져 있다. 이전 강좌에서 살짝 설명하고 넘어갔던 부분이긴 하지만 다시 여기서 정리를 해 보도록 하겠다. gcc 는 4개 까지의 매개변수는 r0, r1, r2, r3 네개의 레지스터를 직접 이용하고 그 이상의 매개변수의 경우에는 스택을 이용한다. 그래서 user_task_3() 함수의 c = mysyscall(1,2,3); 부분에 해당하는 역어셈블 코드를 다시 한번 보도록 하자.

a000beac:	e3a00001 	mov	r0, #1	; 0x1
a000beb0:	e3a01002 	mov	r1, #2	; 0x2
a000beb4:	e3a02003 	mov	r2, #3	; 0x3
a000beb8:	ebffffab 	bl	a000bd6c <mysyscall>
a000bebc:	e1a03000 	mov	r3, r0

1을 r0 에, 2를 r1에, 3을 r2 에 넣고, mysyscall 을 호출 한다. 그리고 함수에 들어갔다 온다음 r0로 부터 리턴값을 받는다. 그러므로 시스템콜 레이어를 훑으면서 r0~r3 까지의 레지스터에 다른 값이 들어가지 않는다면, 매개변수와 리턴값은 온전히 사용자 영역으로 부터 커널영역으로 전달되고, 커널영역으로 부터 사용자 영역까지 반환값도 온전히 전달 될것이다.

여담이지만 이것은 x86에서의 구현도 마찬가지여서, x86 은 eax, ebx, ecx, edx 를 이용해서 매개변수를 전달하고 리턴값 역시 eax를 통해 전달된다. 그래서 리눅스(posix) 시스템콜들은 모두 매개변수가 최대 4개 까지 인것이다. 리턴값 역시 시스템의 1워드 크기로 고정되어 있다.

위와 같이 코드를 작성하였다. 파일이 추가 되었으므로 Makefile을 수정한다. 수정된 Makefile 의 내용은 아래와 같다.

CC = arm-linux-gcc
LD = arm-linux-ld
OC = arm-linux-objcopy
 
CFLAGS    = -nostdinc -I. -I./include 
CFLAGS   += -Wall -Wstrict-prototypes -Wno-trigraphs -O0
CFLAGS   += -fno-strict-aliasing -fno-common -pipe -mapcs-32 
CFLAGS   += -mcpu=xscale -mshort-load-bytes -msoft-float -fno-builtin
 
LDFLAGS   = -static -nostdlib -nostartfiles -nodefaultlibs -p -X -T ./main-ld-script
 
OCFLAGS = -O binary -R .note -R .comment -S
 
CFILES = entry.S navilnux.c navilnux_memory.c navilnux_task.c navilnux_user.c navilnux_lib.S navilnux_sys.c
HFILES = include/navilnux.h include/navilnux_memory.h include/navilnux_task.h include/navilnux_user.h include/navilnux_lib.h include/navilnux_sys.h include/syscalltbl.h
 
all: $(CFILES) $(HFILES)
	$(CC) -c $(CFLAGS) -o entry.o entry.S
	$(CC) -c $(CFLAGS) -o gpio.o gpio.c
	$(CC) -c $(CFLAGS) -o time.o time.c
	$(CC) -c $(CFLAGS) -o vsprintf.o vsprintf.c
	$(CC) -c $(CFLAGS) -o printf.o printf.c
	$(CC) -c $(CFLAGS) -o string.o string.c
	$(CC) -c $(CFLAGS) -o serial.o serial.c
	$(CC) -c $(CFLAGS) -o lib1funcs.o lib1funcs.S
	$(CC) -c $(CFLAGS) -o navilnux.o navilnux.c
	$(CC) -c $(CFLAGS) -o navilnux_memory.o navilnux_memory.c
	$(CC) -c $(CFLAGS) -o navilnux_task.o navilnux_task.c
	$(CC) -c $(CFLAGS) -o navilnux_user.o navilnux_user.c
	$(CC) -c $(CFLAGS) -o navilnux_lib.o navilnux_lib.S
	$(CC) -c $(CFLAGS) -o navilnux_sys.o navilnux_sys.c
	$(LD) $(LDFLAGS) -o navilnux_elf entry.o gpio.o time.o vsprintf.o printf.o string.o serial.o lib1funcs.o navilnux.o navilnux_memory.o navilnux_task.o navilnux_sys.o navilnux_lib.o navilnux_user.o
	$(OC) $(OCFLAGS) navilnux_elf navilnux_img
 
clean:
	rm *.o
	rm navilnux_elf
	rm navilnux_img

Makefile 까지 수정하고 make 를 해서 이미지를 빌드 한 다음, 보드에 다운로드 하고 나빌눅스를 부팅 시켜 보자. 결과 값이 아래와 같이 나온다.

TCB : TASK1 - init PC(a000bda4)          init SP(a04ffffc)                      
TCB : TASK2 - init PC(a000be00)          init SP(a05ffffc)                      
TCB : TASK3 - init PC(a000be88)          init SP(a06ffffc)                      
REAL func TASK1 : a000bda4                                                      
REAL func TASK2 : a000be00                                                      
REAL func TASK3 : a000be88 
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
My Systemcall - 2684387336 , 2 , 3                                              
Syscall return value is 333                                                     
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0   
              :
              :

음... 분명 1,2,3 을 넘겼는데 2684387336 , 2 , 3 이 출력된다. 어디선가 r0 의 값이 바뀌었다. 어디일까. 직감적으로 생각나는 분 계신가? 필자는 이 값을 확인 하는 순간 바로 그곳을 의심했다. 바로!!! 부트로더! 그렇다. 본강좌의 1회에서 부트로더를 수정할때, entry.S의 시작부분으로 Exception Vector Table의 흐름을 넘길때 임시 레지스터로 r0를 사용했었다. 지금 당장 확인 해 보자, 그때 고쳤던 ezboot 의 start 디렉토리의 start.S 의 아랫 부분을 보자.

                     :
                     :
data_abort:
        mov r5, #DEBUG_DATA_ABORT
        bl  led_out
        b   error_loop
 
undefined_instruction:
software_interrupt:
        ldr r0, =0xa0008008
        mov pc, r0
prefetch_abort:
not_used:
IRQ:
        ldr r0, =0xa000800c
        mov pc, r0
FIQ:
        mov r5, #DEBUG_OTHER_EXCEPT
        bl  led_out
        b   error_loop

그렇다. 저렇게 되어 있다. 혹시나 싶어서 0xa0008008 을 10진수로 바꾸어 봤다. 정확하게 2684387336 이 나온다. 이제 더이상 의심의 여지가 없다. 해결 방법은 간단하다. 저기서 r0 를 다른 레지스터. 이왕이면 r0, r1, r2, r3 이 아닌 다른 레지스터로 바꾸면 된다. 필자는 그냥 속편하게 sp 로 바꾸었다.

                     :
                     :
data_abort:
        mov r5, #DEBUG_DATA_ABORT
        bl  led_out
        b   error_loop
 
undefined_instruction:
software_interrupt:
        ldr sp, =0xa0008008
        mov pc, sp
prefetch_abort:
not_used:
IRQ:
        ldr sp, =0xa000800c
        mov pc, sp
FIQ:
        mov r5, #DEBUG_OTHER_EXCEPT
        bl  led_out
        b   error_loop

이제 이지부트를 다시 빌드하고, JTAG를 이용해서 이지부트를 메모리주 주소 0x00000000 에 써 넣도록 하자. 시간은 대략 40분이 걸린다. 수정은 간단하지만 시간이 오래 걸린다.

이지부트 다운로딩이 끝났으면 다시 커널이미지를 빌드하고 부팅해 보자. 의도 했던 결과가 나올 것이다.

TCB : TASK1 - init PC(a000bda4)          init SP(a04ffffc)                      
TCB : TASK2 - init PC(a000be00)          init SP(a05ffffc)                      
TCB : TASK3 - init PC(a000be88)          init SP(a06ffffc)                      
REAL func TASK1 : a000bda4                                                      
REAL func TASK2 : a000be00                                                      
REAL func TASK3 : a000be88 
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
My Systemcall - 1 , 2 , 3                                              
Syscall return value is 333                                                     
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0   
              :
              :

성공이다. 여기서 강좌를 끝낼까 했지만, 노파심에 추가 작업을 조금 더 해 보겠다. 바로 나빌눅스에 시스템콜을 추가 하는 절차 이다. 그래서 mysyscall() 말고 하나의 시스템콜을 더 추가 해 보도록 하겠다.


1. syscalltbl.h 에 시스템콜 번호를 추가 한다.

#ifndef _NAVIL_SYS_TBL
#define _NAVIL_SYS_TBL
 
#define SYS_MYSYSCALL   0
#define SYS_MYSYSCALL4  1
 
#endif

이번에 추가할 시스템콜은 매개변수 4개 짜리 시스템콜을 추가 해 볼 생각이다. 그래서 이름도 sys_mysyscall4로 할것이다. 그래서 시스템콜번호 define 명도 SYS_MYSYSCALL4 이다. 0~254 까지 숫자중 기존의 시스템콜과 겹치지 않는 숫자면 상관이 없다. 가장 좋은 방법은 마지막 시스템콜 번호에 +1 씩 해서 증가 시키는 방법이다. 그래서 1번으로 define 하였다.

2. navilnux_sys.c 에 시스템콜 함수를 작성한다.

#include <navilnux.h>
 
unsigned int navilnux_syscallvec[SYSCALLNUM];
 
int sys_mysyscall(int a, int b, int c)
{
    printf("My Systemcall - %d , %d , %d\n", a, b, c);
    return 333;
}
 
int sys_mysyscall4(int a, int b, int c, int d)
{
    printf("My Systemcall4 - %d , %d , %d , %d\n", a, b, c, d);
    return 3413;
}
 
 
 
 
void syscall_init(void)
{
    navilnux_syscallvec[SYS_MYSYSCALL] = (unsigned int)sys_mysyscall; 
    navilnux_syscallvec[SYS_MYSYSCALL4] = (unsigned int)sys_mysyscall4; 
}

이번에는 매개변수를 4개 받는다. 그래서 그냥 단순하게 이름도 sys_mysyscall4() 이다. 매개변수가 4개라서 뒤에 그냥 4를 붙였다. 이름은 단순할 수록 좋다. :)

3. syscall_init() 함수에서 navilnux_syscallvec 에 새로 추가한 시스템콜의 함수포인터를 등록한다.

void syscall_init(void)
{
    navilnux_syscallvec[SYS_MYSYSCALL] = (unsigned int)sys_mysyscall; 
    navilnux_syscallvec[SYS_MYSYSCALL4] = (unsigned int)sys_mysyscall4; 
}

syscalltbl.h 에서 define 한 시스템콜 번호를 인덱스로 해서 navilnux_syscallvec 에 함수포인터의 주소가 등록 되었다.

4. navilnux_sys.h 에 시스템콜의 프로토타입을 선언한다.

#ifndef _NAVIL_SYS
#define _NAVIL_SYS
 
#include <syscalltbl.h>
 
#define SYSCALLNUM 255
 
void syscall_init(void);
 
 
int sys_mysyscall(int, int, int);
int sys_mysyscall4(int, int, int, int);
 
#endif

커널내부의 다름 부분에서 호출할때에 컴파일러의 심볼테이블에 이름이 등록되어야 하므로 프로토타입을 선언해 놓는다. 사실 없어도 잘 동작하긴 하지만, gcc 컴파일 옵션에 -Wall 을 해 놓으면 경고메시지가 출력된다. 필자는 에러 뿐만 아니라 경고메시지도 하나도 없는 걸 좋아한다. 그래서 프로토타입 선언은 꼭 빼놓지 않고 한다.

5. navilnux_lib.h 에 시스템콜랩퍼함수의 프로토타입을 선언한다.

#ifndef _NAVIL_LIB
#define _NAVIL_LIB
 
extern int mysyscall(int, int, int);
extern int mysyscall4(int, int, int, int);
 
#endif

시스템콜랩퍼함수는 사실상 아무것도 하는 일 없이, 그냥 swi 만 호출했다가 되돌아 오는 일 밖에는 하지 않지만, navilnux_lib.h 에 시스템콜랩퍼함수의 프로토타입을 선언해 주어야 컴파일러는 해당 랩퍼함수가 어떤 타입의 몇개의 매개변수를 전달하는지를 알 수 있다. 랩퍼함수에 대해서는 프로토타입 선언이 의외로 중요하다.

6. navilnux_lib.S 에 시스템콜랩퍼 함수를 작성한다.

#include <syscalltbl.h>
 
.global mysyscall
mysyscall:
    swi SYS_MYSYSCALL 
    mov pc, lr
 
.global mysyscall4
mysyscall4:
    swi SYS_MYSYSCALL4
    mov pc, lr

정말 별로 어렵지 않은 코드이다. 아마 다른 시스템콜이 추가 되더라도 복사해서 붙여넣기 한다음, define 된 레이블이름만 바꿔주는 작업외에는 다른 작업이 필요 없을 것이다.

7. 사용자 테스크에서 사용한다.

              :
              :
void user_task_2(void)
{
    int a, b, c;
 
    a = 1;
    b = 2;
    c = a + b;
 
    while(1){
        printf("TASK2 - a:%p\tb:%p\tc:%p\n", &a, &b, &c);
        c = mysyscall4(4,5,6,7);
        printf("Syscall4 return value is %d\n", c);
        msleep(1000);
    }
}
              :
              :

이번에는 user_task_2() 함수이다. user_task_3() 함수는 시스템콜을 구현하면서 만든 더미 시스템콜을 호출하고 user_task_2() 함수는 이번에 시스템콜 추가 절차를 따라서 새롭게 추가된 mysyscall4() 시스템콜을 호출한다. 이렇게 추가를 하고 가벼운 마음으로 make 를 치고 보드에 커널이미지를 다운로드 한다음 부팅 해 보자.

아래와 같은 메시지가 터미널 프로그램을 통해 출력 된다면 성공적으로 작업한 것이다.

TCB : TASK1 - init PC(a000bda4)          init SP(a04ffffc)                      
TCB : TASK2 - init PC(a000be00)          init SP(a05ffffc)                      
TCB : TASK3 - init PC(a000be88)          init SP(a06ffffc)                      
REAL func TASK1 : a000bda4                                                      
REAL func TASK2 : a000be00                                                      
REAL func TASK3 : a000be88                                                      
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
My Systemcall4 - 4 , 5 , 6 , 7                                                  
Syscall4 return value is 3413                                                   
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
My Systemcall - 1 , 2 , 3                                                       
Syscall return value is 333                                                     
TASK1 - a:a04fffe8      b:a04fffe4      c:a04fffe0                              
TASK2 - a:a05fffe8      b:a05fffe4      c:a05fffe0                              
My Systemcall4 - 4 , 5 , 6 , 7                                                  
Syscall4 return value is 3413                                                   
TASK3 - a:a06fffe8      b:a06fffe4      c:a06fffe0                              
My Systemcall - 1 , 2 , 3                                                       
Syscall return value is 333    
                             :
                             :    

네개의 매개변수 4,5,6,7 이 잘 넘어갔고, sys_mysyscall4() 함수에서 반환하는 3413 이라는 숫자도 잘 넘어온다. 그리고 아까 만들었던 sys_mysyscall() 도 잘 동작한다. 두개의 시스템콜이 잘 동작한다. 그럼 앞으로 n개의 시스템콜을 추가 한다 하더라도 잘 동작할 것임을 경험상 알고 있다.

앞으로 나빌눅스에 추가하게 될, ITC(Inter Task Communication) 이나, 동기화(뮤택스, 세마포어), 메모리동적할당, open(), read(), write(), close()등을 이용하는 디바이스드라이버 모두 시스템콜을 이용해 구현될 예정이다. 그렇기 때문에 이번회에 설명한 시스템콜에 대한 내용은 앞으로의 내용을 이해 하기 위한 기반이 된다. 이해가 안되는 부분이나 강좌에 오류가 있다면 리플을 달아 주시고, 신경써서 읽어 주시기 바란다.

역시나 변함 없이 이번회 까지의 수정한 소스파일을 첨부하오니 공부에 도움이 되시길 바란다.

이 글은 http://raonlife.com/navilera/blog/view/88/ 에 동시 연재 됩니다.

File attachments: 
첨부파일 크기
파일 navilnux_chap14.tgz105.95 KB

댓글

bookgekgom의 이미지

그런데요.

최종적으로 완성된 OS 에 어떤 프로그램을 설치할수있나요?

리눅스프로그램인가요?

아니면 윈도우인가요?

아니면....자기 OS 니까...아무것도 못쓰나요?

마지막 Console base OS 가 되나요? 아니면 GUI 인가요?

궁금궁금

---------------------------------------------------------------------------

허접한 페도라 가이드 http://oniichan.shii.org

---------------------------------------------------------------------------------------------------------------
루비 온 레일즈로 만들고 있는 홈페이지 입니다.

http://jihwankim.co.nr

여러 프로그램 소스들이 있습니다.

필요하신분은 받아가세요.

나빌레라의 이미지

강좌 첫회를 보면 나빌눅스는 임베디드 OS 이고,
커널이미지에 테스크 이미지가 포함되어 동작한다고 씌여 있으며

강좌 내내 사용자 테스크 함수에 기능을 넣고 있는데요.

그리고 임베디드 OS 이기 때문에 쉘이 없고 그래 콘솔이건 GUI 이건 아무데서도 동작하지 않습니다.

강좌를 자세히 읽어주세요.
---
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

verimuch의 이미지

이번회는 특히 어려웠네요;;
---------------------------------
Life is not fair, Get used to it.

---------------------------------
Life is not fair, Get used to it.

나빌레라의 이미지

^^;

어려웠나요..

근데 이번회차에 설명한 시스템콜은 중요해요. 왜냐면 이후부터 강좌 끝날때까지 추가되는

모든 기능이 전부 시스템콜계층으로 연결되거든요...^^;

----
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

verimuch의 이미지

계속 따라하면서 조금씩 치고 있었거든요
그런데 이번회에만 계속 에러가 나서
처음부터 차근차근 읽다보니 제가 syscall_init()함수가 navilnux_init()함수에서
호출된다는 것을 읽기만 하고 넘어갔더군요;;
항상 수정되는 부분이 표시되어 있어서 별생각이 없었는데...하핫
고생 좀 했네요;;
---------------------------------
Life is not fair, Get used to it.

---------------------------------
Life is not fair, Get used to it.

나빌레라의 이미지

달랑 한줄 추가 된거 가지고 소스코드를 계속 리스팅 하려다 보니
괜시 분량늘리기 하고 있는 것 같아서 그냥 설명만하고 넘어갔는데..

코드리스트 넣을걸 그랬나 봐요..^^;

----------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

kooprs227의 이미지

잘 따라해보고 있습니다... 포스팅 하신 후 너무 오래 됐지만 OS를 처음 공부하는 입장에서 너무나 유용한 자료 같습니다.

덕분에 EZ-X5보드도 중고로 구입해서 재미있게 따라하고 있어요 ^^ 책은.. 학교 도서관에 있어서 빌렸어요 ㅠㅠ;

근데 이번 회차에 마지막에 ldmfd sp!, {pc}^ 이 부분이 제꺼에는 안돌아가더라구요?

자꾸 리턴 값을 출력 안하고 다운이 돼서.. 하루종일 삽질 하다가 그 구문을 위에서 합쳐 봤는데 잘 되네요..

ldmfd sp!, {r1-r14, pc}^ 이렇게요 이유는 잘 모르겠습니다 ㅠㅠ

여하튼 늦었지만 너무너무 좋은 포스팅이라 몇년이 지난 후인데도 배우는 사람들한테 도움이 팍팍! 됩니다. 감사합니다.
한번 뵙고 싶은 정도로 감사해요 ㅎㅎㅎㅎㅎ 아, 혹시 뵐 수 있을 지도.. 아직 S사에 계시면요 ㅎㅎㅎ

댓글 달기

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
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.