리눅스 커널은 운영 체제의 핵심으로, 안정성이 가장 중요합니다. 그러나 C언어로 작성된 커널 코드에서 버그가 발생하면 커널 패닉으로 시스템이 정지될 수 있습니다. 본 기사에서는 커널 패닉의 개념부터 C언어로 문제를 분석하고 디버깅하는 방법까지 자세히 설명합니다. 이를 통해 리눅스 커널 개발자와 시스템 엔지니어가 문제 해결 능력을 향상시킬 수 있습니다.
리눅스 커널 패닉의 개념과 원인
리눅스 커널 패닉은 시스템이 복구할 수 없는 치명적인 오류에 직면했을 때 발생하는 상태로, 커널이 더 이상 안전하게 실행될 수 없을 때 시스템을 멈추도록 설계된 보호 메커니즘입니다.
커널 패닉의 주요 원인
- 메모리 접근 오류: 잘못된 메모리 참조 또는 NULL 포인터 접근.
- 디바이스 드라이버 결함: 호환되지 않는 드라이버나 잘못된 코드로 인한 오류.
- 시스템 자원 부족: 메모리 부족, CPU 과부하로 인해 작업이 처리되지 않음.
- 커널 모듈 충돌: 잘못 설계된 모듈로 인해 커널 내에서 충돌 발생.
커널 패닉과 Oops 메시지
커널 패닉은 종종 Oops 메시지로 시작됩니다. Oops는 시스템에 심각한 문제가 있지만, 완전히 정지하지는 않았다는 신호입니다. 그러나 연속적인 Oops는 커널 패닉으로 이어질 가능성이 높습니다.
커널 패닉의 예시
다음은 메모리 접근 오류로 인해 발생한 커널 패닉 로그의 예시입니다:
Kernel panic - not syncing: Attempted to kill init!
Call Trace:
[<ffffffff8106b4a5>] panic+0x78/0x123
[<ffffffff810c8abc>] oom_kill_process+0x4cc/0x520
이와 같은 로그는 디버깅 시작의 단서를 제공합니다. 커널 패닉을 분석하기 위해 로그를 정밀하게 검토하고 주요 원인을 파악하는 것이 필수적입니다.
C언어로 커널 디버깅을 시작하기
리눅스 커널 디버깅은 복잡하지만, C언어와 적절한 도구를 사용하면 효과적으로 문제를 분석할 수 있습니다. 이 섹션에서는 디버깅 환경을 설정하고 기본적인 디버깅 기법을 소개합니다.
디버깅 환경 설정
- 디버깅 가능한 커널 빌드
커널 디버깅을 위해서는 디버그 심볼이 포함된 커널을 빌드해야 합니다.CONFIG_DEBUG_INFO
옵션을 활성화하고, 최적화 수준을 낮추기 위해-O2
대신-O0
로 설정합니다.
make menuconfig
# Kernel hacking -> Compile the kernel with debug info (enable CONFIG_DEBUG_INFO)
make -j$(nproc)
- QEMU와 같은 가상 환경 사용
실제 하드웨어 대신 QEMU 같은 가상 머신에서 커널을 실행하면 안전하고 효율적인 디버깅이 가능합니다.
qemu-system-x86_64 -kernel bzImage -append "console=ttyS0" -nographic
- GDB 서버 연결
QEMU의 GDB 서버를 활용해 원격 디버깅이 가능합니다.
qemu-system-x86_64 -s -S -kernel bzImage
gdb vmlinux
target remote localhost:1234
디버깅 도구
- GDB (GNU Debugger): C언어로 작성된 커널 코드를 소스 레벨에서 디버깅할 수 있습니다.
- Kgdb: 커널 내부에서 실행되는 디버거로, 커널 패닉과 같은 문제를 분석하는 데 유용합니다.
- Ftrace: 함수 호출을 추적해 커널 동작을 분석할 수 있습니다.
핵심 디버깅 기법
- breakpoint 설정
특정 함수나 메모리 주소에서 코드 실행을 멈추고 상태를 확인합니다.
break start_kernel
continue
- 메모리 검사
변수 값과 메모리 주소를 확인해 오류를 진단합니다.
print var_name
x/16xw 0xaddress
- 함수 호출 스택 확인
오류 발생 지점을 파악하기 위해 호출 스택을 추적합니다.
backtrace
이러한 설정과 도구를 사용하면 커널 디버깅을 보다 체계적으로 진행할 수 있습니다. 다음 섹션에서는 커널 패닉 로그를 이해하고 분석하는 방법을 다룹니다.
커널 패닉 로그 이해하기
커널 패닉 로그는 문제 원인을 분석하는 데 핵심적인 정보를 제공합니다. 로그를 읽고 이해하면 디버깅을 시작하는 데 필요한 단서를 얻을 수 있습니다.
로그의 주요 구성 요소
- 오류 메시지
커널 패닉의 이유를 간략히 설명하는 메시지로, 문제의 성격을 알려줍니다.
Kernel panic - not syncing: Fatal exception in interrupt
- Call Trace
함수 호출 스택 정보를 포함하며, 오류가 발생한 코드 흐름을 보여줍니다.
Call Trace:
[<ffffffff8106b4a5>] panic+0x78/0x123
[<ffffffff810c8abc>] handle_mm_fault+0x2fc/0x5e0
[<ffffffff81003458>] do_page_fault+0x138/0x390
- CPU와 PID 정보
문제가 발생한 CPU와 프로세스 정보를 나타냅니다.
CPU: 0 PID: 1234 Comm: my_program
- 메모리 덤프
오류 발생 당시 메모리 상태를 보여줍니다. 종종 디버깅의 중요한 단서가 됩니다.
로그 분석 단계
- 오류 메시지 확인
첫 번째 메시지를 통해 문제의 범주를 식별합니다. 예를 들어, “not syncing”은 복구 불가능한 오류임을 나타냅니다. - Call Trace 추적
호출된 함수들의 순서를 확인하여 문제 발생 지점을 파악합니다. 아래는 Call Trace 분석 예시입니다:
[<ffffffff8106b4a5>] panic+0x78/0x123 # panic 함수 호출
[<ffffffff810c8abc>] oom_kill_process+0x4cc/0x520 # 메모리 부족 처리 중 발생
[<ffffffff81003458>] do_page_fault+0x138/0x390 # 페이지 폴트 발생
이 경우 메모리 부족으로 인한 페이지 폴트가 문제 원인으로 보입니다.
- 관련 코드 검토
로그에 표시된 함수와 관련된 소스 코드를 확인합니다.
void __oom_kill_process(struct task_struct *p) {
if (!p->mm)
return; // 프로세스가 메모리를 사용하지 않음
// ...
}
- 메모리 덤프 분석
메모리 상태와 레지스터 값을 확인하여 잘못된 데이터나 메모리 손상을 추적합니다.
실습 예시: 커널 패닉 로그 분석
로그를 분석하여 다음과 같은 문제를 진단했다고 가정합니다:
- 원인: 잘못된 포인터 접근으로 인한 NULL 참조
- 해결 방법: 해당 코드에서 포인터 유효성을 확인하도록 수정
커널 로그는 디버깅 과정의 출발점입니다. 이를 효과적으로 활용하면 문제를 신속히 진단할 수 있습니다. 다음 섹션에서는 커널 패닉을 유발하는 일반적인 코드를 살펴봅니다.
커널 오류의 흔한 사례
커널 패닉은 다양한 원인으로 발생하지만, 대부분 특정한 코드 결함에서 비롯됩니다. 이 섹션에서는 커널 패닉을 유발하는 일반적인 사례를 살펴보고, 이러한 문제를 예방하거나 해결하는 방법을 설명합니다.
1. NULL 포인터 참조
잘못된 포인터 초기화로 인해 NULL 포인터를 참조하면 커널 패닉이 발생할 수 있습니다.
int *ptr = NULL;
*ptr = 42; // NULL 참조로 패닉 발생
- 예방 방법: 포인터를 사용하기 전에 NULL 여부를 항상 확인합니다.
if (ptr) {
*ptr = 42;
}
2. 잘못된 메모리 접근
커널은 보호된 메모리 영역에 접근할 수 없으며, 잘못된 접근은 페이지 폴트를 일으킵니다.
char *invalid_ptr = (char *)0xdeadbeef;
*invalid_ptr = 'a'; // 잘못된 메모리 접근
- 예방 방법: 올바른 주소를 참조하도록 보장하고, 메모리 매핑을 검증합니다.
3. 스택 오버플로
재귀 호출이 무한히 반복되거나, 스택 메모리 할당이 과도하면 스택 오버플로로 인해 시스템이 정지할 수 있습니다.
void infinite_recursion() {
infinite_recursion(); // 무한 재귀 호출
}
- 예방 방법: 재귀 호출에 종료 조건을 추가하고, 스택 사용량을 제한합니다.
4. 경쟁 상태 (Race Condition)
멀티스레드 환경에서 자원 동기화가 부족하면 예상치 못한 동작이 발생할 수 있습니다.
void update_shared_resource() {
shared_var++;
}
- 예방 방법: 스핀락이나 뮤텍스를 사용해 동기화 메커니즘을 구현합니다.
spin_lock(&lock);
shared_var++;
spin_unlock(&lock);
5. 부적절한 커널 모듈 제거
로드된 커널 모듈을 제거하는 과정에서 의존 관계를 고려하지 않으면 시스템 충돌이 발생할 수 있습니다.
- 예방 방법: 모듈 제거 전에 의존성을 확인하고 안전하게 언로드합니다.
6. 자원 누수
파일 핸들, 메모리, 락 등이 제대로 해제되지 않으면 시스템 안정성이 저하될 수 있습니다.
void example() {
char *buffer = kmalloc(1024, GFP_KERNEL);
// 메모리 해제 누락
}
- 예방 방법: 자원을 사용한 후 반드시 해제합니다.
kfree(buffer);
실습 예시: 코드 수정으로 오류 해결
다음은 NULL 포인터 참조 문제를 수정한 코드입니다:
int *ptr = NULL;
// 수정 후
if (!ptr) {
ptr = kmalloc(sizeof(int), GFP_KERNEL);
}
*ptr = 42; // 안전한 접근
이러한 사례와 해결책을 통해 커널 패닉을 예방할 수 있습니다. 다음 섹션에서는 커널 메모리 관리와 관련된 문제를 다룹니다.
커널 메모리 관리와 문제 해결
리눅스 커널에서 메모리 관리는 시스템 안정성과 성능에 직접적으로 영향을 미칩니다. 잘못된 메모리 관리는 커널 패닉이나 데이터 손상을 초래할 수 있습니다. 이 섹션에서는 커널 메모리 관리의 기본 원리와 디버깅 방법을 살펴봅니다.
커널 메모리의 구조
리눅스 커널은 메모리를 크게 두 가지로 구분하여 관리합니다.
- 유저 공간(User Space)
- 애플리케이션이 사용하는 메모리 영역.
- 커널과 분리되어 보호됨.
- 커널 공간(Kernel Space)
- 커널이 사용하는 메모리 영역.
- 디바이스 드라이버와 모듈이 이 공간에서 실행됨.
커널 메모리 할당
커널에서는 kmalloc
, vmalloc
, kfree
등의 함수로 메모리를 동적으로 할당 및 해제합니다.
- kmalloc: 물리적으로 연속된 메모리를 할당합니다.
- vmalloc: 가상 메모리를 할당하며, 물리 메모리는 연속적이지 않을 수 있습니다.
- kfree:
kmalloc
로 할당된 메모리를 해제합니다.
예시:
char *buffer = kmalloc(1024, GFP_KERNEL);
if (!buffer) {
pr_err("Memory allocation failed\n");
return -ENOMEM;
}
kfree(buffer);
메모리 관리 문제
- 메모리 누수(Memory Leak)
할당된 메모리를 해제하지 않아 시스템 리소스가 고갈되는 문제입니다.
- 해결 방법:
kfree
를 사용해 모든 할당 메모리를 반드시 해제합니다.
- Double Free 오류
이미 해제된 메모리를 다시 해제하면 시스템이 비정상 동작할 수 있습니다.
- 해결 방법: 메모리를 해제한 후 포인터를 NULL로 초기화합니다.
kfree(buffer);
buffer = NULL;
- Use-After-Free
해제된 메모리를 참조하는 경우 발생합니다.
- 해결 방법: 해제 후 해당 포인터를 사용하지 않도록 주의합니다.
커널 메모리 디버깅
- KASAN (Kernel Address Sanitizer)
메모리 관련 오류를 탐지하는 강력한 디버깅 도구입니다.
make menuconfig
# Enable Kernel Address Sanitizer (KASAN)
- SLUB 디버거
메모리 누수를 탐지하기 위해SLUB_DEBUG
옵션을 활성화합니다.
echo 1 > /sys/kernel/slab/<slab_name>/trace
예시: 메모리 누수 문제 해결
다음은 메모리 누수가 발생하는 코드와 이를 수정한 코드입니다.
// 문제 코드
char *buffer = kmalloc(1024, GFP_KERNEL);
// 메모리 해제 누락
// 수정된 코드
char *buffer = kmalloc(1024, GFP_KERNEL);
if (!buffer) {
return -ENOMEM;
}
kfree(buffer);
최적화와 예방
- 필요한 만큼만 메모리를 할당하고, 사용이 끝난 메모리를 즉시 해제합니다.
- 디버깅 도구를 활용해 메모리 문제를 조기에 발견합니다.
커널 메모리 관리는 시스템 안정성을 보장하는 핵심 요소입니다. 다음 섹션에서는 커널 디버깅 도구를 활용하는 방법에 대해 알아봅니다.
커널 디버깅 도구 활용법
리눅스 커널 디버깅은 문제의 원인을 신속하게 파악하고 해결하기 위해 다양한 도구를 활용합니다. 이 섹션에서는 커널 디버깅에 사용되는 주요 도구와 활용 방법을 소개합니다.
1. GDB (GNU Debugger)
GDB는 커널 디버깅에서 널리 사용되는 도구로, QEMU와 같은 가상 머신과 연동하여 커널의 실행 상태를 분석할 수 있습니다.
- 설정 방법:
QEMU 실행 시 GDB 서버를 활성화합니다.
qemu-system-x86_64 -s -S -kernel bzImage
gdb vmlinux
target remote localhost:1234
- 주요 명령어:
break
: 특정 함수나 위치에서 중단점 설정.continue
: 프로그램 실행 재개.backtrace
: 호출 스택 추적.print
: 변수 값 확인.
2. Kgdb (Kernel GNU Debugger)
Kgdb는 커널 내부에서 실행되는 디버거로, 디버깅 중에도 커널이 실행 상태를 유지합니다.
- 활성화 방법:
커널 설정에서CONFIG_KGDB
와CONFIG_KGDB_SERIAL_CONSOLE
옵션을 활성화합니다.
make menuconfig
# Kernel hacking -> Kernel debugging
- 사용 사례:
Kgdb를 통해 동작 중인 커널에서 변수 상태를 점검하고, 커널 패닉 상황을 분석할 수 있습니다.
3. Crash 유틸리티
Crash는 커널의 메모리 덤프를 분석하는 도구로, 커널 패닉 후의 상태를 조사하는 데 유용합니다.
- 설치 및 실행:
crash vmlinux /path/to/core
- 주요 기능:
bt
: 호출 스택 정보 확인.ps
: 실행 중인 프로세스 정보 표시.kmem
: 커널 메모리 상태 분석.
4. Ftrace
Ftrace는 함수 호출과 실행 시간을 추적하여 커널 동작을 분석하는 데 사용됩니다.
- 활성화 방법:
Ftrace 설정을 위해 커널 옵션에서CONFIG_FUNCTION_TRACER
를 활성화합니다.
echo function > /sys/kernel/debug/tracing/current_tracer
- 사용 사례:
특정 함수 호출을 추적하고, 성능 병목 지점을 파악합니다.
5. Dmesg 로그
Dmesg는 커널의 로그 메시지를 확인하는 기본 도구입니다.
- 사용 방법:
dmesg | grep -i "panic"
- 분석 사례:
커널 패닉 직전에 출력된 로그를 확인하여 원인을 추적합니다.
6. SystemTap
SystemTap은 커널 이벤트를 실시간으로 추적하는 도구로, 고급 분석에 적합합니다.
- 설치 및 사용:
stap -e 'probe kernel.function("do_fork") { println("Fork called") }'
- 적용 사례:
함수 호출 빈도를 모니터링하거나 성능 문제를 분석합니다.
실습 예시: GDB를 사용한 디버깅
다음은 GDB로 커널 함수 호출 스택을 분석하는 예시입니다.
target remote localhost:1234
break start_kernel
continue
backtrace
디버깅 팁
- 디버깅 도구를 조합하여 다양한 문제를 포괄적으로 분석합니다.
- 디버깅 로그와 상태 정보를 기록하여 패턴을 확인합니다.
이 도구들을 활용하면 커널 디버깅 과정을 체계적으로 진행할 수 있습니다. 다음 섹션에서는 커널 모듈 디버깅의 기초를 다룹니다.
커널 모듈 디버깅의 기초
커널 모듈은 동적으로 로드 및 언로드할 수 있는 커널 확장 코드입니다. 커널 모듈 디버깅은 커널 전체를 빌드하지 않고도 개별 모듈의 문제를 분석할 수 있는 유용한 방법입니다. 이 섹션에서는 커널 모듈 디버깅의 기본 과정을 소개합니다.
1. 커널 모듈의 구조
커널 모듈은 일반적으로 다음 두 가지 함수로 구성됩니다.
- init 함수: 모듈이 로드될 때 호출됩니다.
- exit 함수: 모듈이 언로드될 때 호출됩니다.
예시:
#include <linux/module.h>
#include <linux/init.h>
static int __init my_module_init(void) {
pr_info("My Module Loaded\n");
return 0;
}
static void __exit my_module_exit(void) {
pr_info("My Module Unloaded\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple Kernel Module");
2. 모듈 컴파일 및 로드
- 모듈 컴파일:
모듈은 커널 빌드 시스템을 사용해 컴파일합니다.
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
- 모듈 로드:
insmod
명령어를 사용해 모듈을 커널에 로드합니다.
sudo insmod my_module.ko
- 모듈 언로드:
rmmod
명령어를 사용해 모듈을 제거합니다.
sudo rmmod my_module
3. 디버깅 기법
- printk 로그 사용
printk
는 모듈에서 실행 상태를 기록하는 기본적인 방법입니다.
pr_info("Debug: Variable x = %d\n", x);
로그는 dmesg
명령어로 확인할 수 있습니다.
dmesg | tail
- GDB와 QEMU를 활용한 디버깅
커널 모듈이 로드된 상태에서 GDB를 사용해 문제를 분석할 수 있습니다. - Ftrace 활용
Ftrace를 사용해 모듈 내 함수 호출을 추적할 수 있습니다.
echo my_module_function > /sys/kernel/debug/tracing/set_ftrace_filter
echo function > /sys/kernel/debug/tracing/current_tracer
cat /sys/kernel/debug/tracing/trace
4. 디버깅 중 자주 발생하는 문제
- 모듈 로드 실패: 종속성 부족이나 컴파일 옵션 오류로 인해 발생.
- 해결:
modinfo
로 모듈 정보를 확인하고, 필요한 커널 심볼이 존재하는지 점검합니다. - Segmentation Fault: 잘못된 포인터 접근.
- 해결: 포인터를 초기화하고 유효성을 확인합니다.
5. 실습 예시: 로그 분석을 통한 디버깅
다음은 printk
로그를 활용해 오류를 진단하는 예시입니다.
static int __init my_module_init(void) {
int *ptr = NULL;
pr_info("My Module Init\n");
*ptr = 10; // NULL 포인터 참조
return 0;
}
로그 출력:
[ 1234.567890] My Module Init
[ 1234.567891] Kernel panic - not syncing: Attempted to dereference NULL pointer
6. 안전한 모듈 디버깅 팁
- 모듈 개발 시 보호 매크로를 사용해 코드 안정성을 확보합니다.
- 디버깅이 완료될 때까지
printk
메시지를 충분히 기록합니다. - 모듈 디버깅 시 커널 공간에서 작동하므로 주의가 필요합니다.
커널 모듈 디버깅을 통해 커널 기능을 확장하고 안정성을 확보할 수 있습니다. 다음 섹션에서는 디버깅을 통해 시스템 성능을 최적화한 응용 사례를 살펴봅니다.
응용 사례: 시스템 리소스 최적화
커널 디버깅은 단순히 오류를 해결하는 데 그치지 않고, 시스템 성능을 개선하고 리소스를 최적화하는 데도 중요한 역할을 합니다. 이 섹션에서는 디버깅을 통해 리소스 활용도를 높이고 시스템 성능을 최적화한 사례를 소개합니다.
1. 사례: CPU 스케줄링 최적화
문제: CPU 스케줄링에서 특정 작업이 과도한 시간을 점유해 시스템 전체 성능 저하 발생.
원인 분석:
- Ftrace를 활용해 함수 실행 시간을 분석한 결과, 특정 루틴이 불필요한 반복 연산을 수행하고 있음이 확인됨.
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo schedule > /sys/kernel/debug/tracing/set_ftrace_filter
cat /sys/kernel/debug/tracing/trace
해결:
- 반복 연산을 최적화하고, 스케줄링 우선순위를 조정.
- 결과적으로 CPU 사용률이 20% 감소하고, 작업 처리량이 15% 증가.
2. 사례: 메모리 누수 방지
문제: 메모리 누수로 인해 장기 실행 시스템에서 OOM(Out of Memory) 오류 발생.
원인 분석:
- SLUB 디버거를 활성화하여 메모리 할당과 해제 상태를 추적.
echo 1 > /sys/kernel/slab/my_slab/trace
- 할당된 메모리가 해제되지 않고 누적되고 있음이 확인됨.
해결:
- 누수 원인 코드 수정: 누락된
kfree
호출 추가. - 수정 후 메모리 사용량이 안정적으로 유지됨.
3. 사례: 디바이스 드라이버 성능 개선
문제: 특정 디바이스 드라이버가 데이터 전송 속도에서 병목 현상 발생.
원인 분석:
dmesg
로그에서 드라이버가 I/O 요청을 효율적으로 처리하지 못하는 것을 확인.- GDB로 디버깅하여 큐 관리 알고리즘에 비효율적인 코드 발견.
해결:
- 큐 관리 알고리즘을 최적화하고, I/O 요청 병렬 처리를 구현.
- 수정 후 데이터 전송 속도가 30% 향상.
4. 사례: 커널 모듈 충돌 문제 해결
문제: 커널 모듈 간 의존성 문제로 시스템 충돌 발생.
원인 분석:
- Crash 유틸리티로 커널 덤프를 분석하여 의존성 부족으로 인한 심볼 충돌 확인.
crash vmlinux /path/to/core
sym -a
해결:
- 의존 관계를 명시적으로 설정하고, 모듈 로드 순서를 조정.
- 시스템 안정성이 개선되고 충돌 발생 횟수가 0으로 감소.
5. 실습 예시: 네트워크 성능 최적화
문제: 네트워크 처리량이 낮아 데이터 전송이 지연됨.
- BPF(Berkeley Packet Filter)를 사용해 패킷 처리 시간을 분석.
bpftool prog show
bpftool map dump id <map_id>
- 불필요한 패킷 검사 루틴을 제거하고 커널 네트워크 스택을 최적화.
결과:
- 네트워크 처리량이 25% 증가하고, 데이터 전송 지연이 30% 감소.
최적화와 디버깅의 시너지 효과
커널 디버깅 도구를 활용한 최적화는 시스템 안정성과 성능을 동시에 개선합니다. 디버깅을 통해 리소스 사용 패턴을 파악하고, 병목 현상을 해결하면 시스템의 전반적인 효율성이 높아집니다.
다음 섹션에서는 본 기사를 요약하여 주요 내용을 정리합니다.
요약
본 기사에서는 리눅스 커널 패닉과 디버깅 과정을 C언어 관점에서 심도 있게 다뤘습니다. 커널 패닉의 개념과 주요 원인을 이해하고, GDB, Kgdb, Crash, Ftrace와 같은 디버깅 도구를 활용해 문제를 분석하고 해결하는 방법을 설명했습니다.
또한, 메모리 관리, 커널 모듈 디버깅, 그리고 디버깅을 통해 시스템 리소스를 최적화한 다양한 응용 사례를 제시했습니다. 이를 통해 리눅스 커널 개발자와 시스템 엔지니어가 안정적이고 효율적인 시스템을 구축하는 데 필요한 지식을 제공합니다.
커널 디버깅은 복잡하지만, 체계적인 접근과 적절한 도구 활용으로 문제 해결과 최적화 모두를 달성할 수 있습니다.