Rn's profile image

Rn

January 29, 2025 00:00

ptrace로 프로세스 제어 및 응용하기

Linux , ptrace , debugging , process , security

Intro

안녕하세요. 이번 글에서는 리눅스에서 제공하는 시스템 콜인 ptrace를 활용하여 프로세스를 제어하는 방법을 다루어보려 합니다. ptrace는 프로세스(process)를 추적(trace)하고 디버깅하는 데 사용되는 강력한 기능을 제공합니다. 우리가 흔히 사용하는 GDB(GNU Debugger)도 내부적으로 ptrace를 사용하여 디버깅 기능을 수행합니다.

본 글에서는 ptrace가 제공하는 여러 기능을 살펴본 후, 이를 응용하여 간단한 예제 프로그램을 만들어 보겠습니다.

본 글은 기본적인 시그널 제어 및 프로세스 핸들링을 이해하고 있다는 가정 하에 작성되었습니다. 시그널과 프로세스에 대한 기본적인 이해가 없는 경우, 먼저 해당 내용을 숙지하신 후 읽어주시기 바랍니다.

ptrace

리눅스 환경에서 디버깅, 즉 프로세스의 내부 동작을 확인하고 수정하기 위해서는 운영 체제가 제공하는 다양한 기능을 활용해야 합니다. 그중 하나가 바로 ptrace 시스템 콜입니다. 이 시스템 콜은 사용자가 특정 프로세스를 추적(trace) 할 수 있도록 해주며, 브레이크포인트 설정이나 메모리 읽기/쓰기, 레지스터 값 변경, 단일 스텝(single-step) 실행 등 다양한 기능을 제공합니다.

해당 기능을 이용해 다음과 같은 상황에서 주로 사용합니다.

  • 디버깅: GDB, strace 같은 디버깅 툴들이 내부적으로 ptrace를 사용합니다.
  • 보안: 임의의 프로그램을 실행할 때 ptrace를 사용해 프로그램의 동작을 제한할 수 있습니다.
  • 프로파일링: 프로세스의 동작을 기록하여 성능 분석에 활용할 때도 쓰일 수 있습니다.

[주의] ptrace를 사용하면 프로세스 내부의 거의 모든 정보에 접근이 가능하기 때문에, 잘못된 사용은 보안상 큰 문제가 될 수 있습니다. 예를 들어, 다른 사용자의 프로세스에 임의로 접속해 메모리를 변경하는 것은 보안상 허용되어서는 안 됩니다. 따라서 보통 ptrace를 사용할 때에는 루트 권한이 요구되거나, 혹은 추적하려는 프로세스와 같은 사용자의 소유여야 하는 등의 제한이 있습니다.

기본 동작 원리

프로세스 트레이싱 흐름

ptrace의 간단한 흐름은 다음과 같이 요약할 수 있습니다.

  1. 부모 프로세스(Tracer)자식 프로세스(Tracee)를 생성하거나, 이미 실행 중인 프로세스에 attach 하여 추적을 시작합니다.
  2. 자식 프로세스의 수행 흐름이 특정 이벤트(시스템 콜 진입/종료, 시그널 발생, 단일 스텝 등)에 도달하면, 자식 프로세스는 중단(stop) 상태가 됩니다.
  3. 이때, 부모 프로세스는 ptrace 시스템 콜을 통해 자식 프로세스의 메모리나 레지스터를 읽고 쓰는 등 필요한 작업을 수행할 수 있습니다.
  4. 이후, 부모 프로세스가 다시 자식 프로세스를 실행(PTRACE_CONT 등)하게 하면 자식 프로세스는 중단된 지점부터 계속 실행됩니다.

즉, 부모 프로세스가 자식 프로세스의 실행 흐름을 일시 정지시키고(ptrace), 내부 상태를 들여다보거나 수정한 다음, 다시 실행을 재개시키는 방식입니다.

시그널 처리

리눅스 프로세스가 실행 중 시그널을 받으면, 일반적으로 해당 시그널을 처리하거나 종료하게 됩니다. 그러나 ptrace로 추적 중인 프로세스에 시그널이 들어오면, 우선 부모 프로세스(디버거)가 이를 확인하고, 그 후에 자식 프로세스에 시그널 전달 여부를 결정할 수 있습니다. 예를 들어, 디버거는 SIGSEGV 시그널이 왔을 때 추적을 중단해 디버거에게 제어를 넘긴 후, 실제로 자식 프로세스를 종료시킬지, 아니면 시그널 전달을 무시하고 계속 실행할지 선택할 수 있습니다.

주요 ptrace 요청(Request)

ptrace 사용 시, ptrace(request, pid, addr, data) 형식의 함수를 호출하게 됩니다(일반적으로 C/C++에서 #include <sys/ptrace.h>를 통해 사용). 주요 request 값들은 다음과 같습니다.

  1. PTRACE_TRACEME
    • 현재 프로세스가 부모 프로세스에 의해 추적될 수 있도록 설정합니다. 보통 자식 프로세스가 이 옵션을 스스로 호출하고, 이후 부모 프로세스가 자식을 기다리면서(waitpid) ptrace를 통해 본격적인 디버깅을 시작합니다.
  2. PTRACE_ATTACH
    • 이미 실행 중인 프로세스에 attach(추적)합니다. GDB로 특정 프로세스를 attach 하는 것이 이 메커니즘과 동일합니다.
  3. PTRACE_DETACH
    • 추적을 중단(detach)하고, 자식 프로세스가 정상적으로 실행되도록 합니다.
  4. PTRACE_CONT
    • 중단된 자식 프로세스를 계속 실행(continue)시킵니다.
  5. PTRACE_SINGLESTEP
    • 자식 프로세스를 한 명령어씩 단일 스텝으로 실행시킵니다. 이때 자식 프로세스는 한 번의 CPU 명령어가 실행된 후 다시 중단됩니다.
  6. PTRACE_SYSCALL
    • 자식 프로세스가 시스템 콜 진입 또는 종료 시점마다 중단되도록 하여 부모가 개입할 수 있도록 합니다.
  7. PTRACE_GETREGS, PTRACE_SETREGS
    • 자식 프로세스(Tracee)의 일반 레지스터 값을 가져오거나 설정합니다.
  8. PTRACE_PEEKTEXT, PTRACE_PEEKDATA
    • 자식 프로세스의 메모리를 읽어옵니다. 텍스트 영역, 데이터 영역 구분 없이 사용할 수 있지만 전통적인 매크로 이름에 따라 구분 지어져 있습니다.
  9. PTRACE_POKETEXT, PTRACE_POKEDATA
    • 자식 프로세스의 메모리에 데이터를 써넣습니다.

이 외에도 여러 가지 옵션이 존재하지만, 상기 나열한 것들이 대부분의 디버깅 시나리오에서 자주 쓰이는 핵심적인 요청들입니다.

Requirements

ptrace는 프로세스를 직접적으로 제어하는 함수이므로 운영체제 및 CPU 아키텍쳐에 따라 동작이 달라지거나, 지원하지 않을 수 있습니다. 따라서, 본 글에서는 아래 요구사항을 가정하고 진행하겠습니다.

개발 환경

  • 운영 체제: Ubuntu, Debian, Fedora 등 리눅스 계열(본 글에서는 Ubuntu 20.04/22.04 정도를 예시로 가정)
  • CPU 아키텍쳐: x86-64
  • 컴파일러: GCC

권한 문제

  • ptrace는 보안상의 이유로 제한이 있을 수 있습니다. 예를 들어, Ubuntu에서는 실행 중인 프로세스에 ptrace attach 사용을 기본적으로 제한할 수 있습니다. 이를 해결하기 위해서 root 권한으로 사용하거나, /proc/sys/kernel/yama/ptrace_scope 값을 조정해야 합니다.
    • sudo sysctl -w kernel.yama.ptrace_scope=0
    • 위와 같은 명령어를 실행하면 전역적으로 ptrace 제한이 풀립니다(단, 보안 위험이 있으므로 주의).

Example 1: 자식 프로세스 추적하기 및 시그널 핸들링

먼저, 가장 기본적인 시나리오인 “자식 프로세스를 생성(fork)해서 부모 프로세스가 자식 프로세스를 추적 및 시그널 핸들링”하는 방식을 살펴보겠습니다.

코드 예시

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <stdio.h>

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>

int main() {
    pid_t child = fork();
    if (child == -1) {
        perror("fork");
        return -1;
    }

    if (child == 0) { // 자식 프로세스
        // 부모가 자신을 추적할 수 있도록 설정
        if (ptrace(PTRACE_TRACEME, 0, NULL, NULL) == -1) {
            perror("ptrace");
            exit(1);
        }
        // 부모가 attach하도록 대기하기 위해 시그널 발생시킴
        // 보통 raise(SIGSTOP)을 쓰거나, execve로 새 프로그램을 실행하기도 함
        raise(SIGSTOP);

        // 자식이 하는 임의 작업
        for (int i = 0; i < 5; ++i) {
            printf("Child: i = %d\n", i);
            sleep(1);
            raise(SIGINT); // 매 카운터 출력 마다 SIGINT 시그널 임의 발생
        }
        return 0;
    } else { // 부모 프로세스
        int status;
        // 자식이 SIGSTOP으로 중단될 때까지 대기
        waitpid(child, &status, 0);
        if (WIFSTOPPED(status)) {
            printf("Parent: child has stopped, start tracing...\n");
        }

        // 자식 프로세스가 종료될 때까지 추적
        for (; ;) {
            // 자식 프로세스 실행 계속
            if (ptrace(PTRACE_CONT, child, NULL, NULL) == -1) {
                perror("ptrace");
                break;
            }

            // 자식 프로세스의 이벤트 대기
            waitpid(child, &status, 0);

            // 자식 프로세스가 죽었는지 확인
            if (WIFSIGNALED(status)) {
                printf("Parent: child killed with signal: %d\n", WTERMSIG(status));
                break;
            }

            // 자식 프로세스가 종료되었는지 확인
            if (WIFEXITED(status)) {
                printf("Parent: child exited with status %d\n", WEXITSTATUS(status));
                break;
            }

            // 중단된 경우, 부모 프로세스에서 추가 작업 가능
            if (WIFSTOPPED(status)) {
                printf("Parent: child stopped by signal %d\n", WSTOPSIG(status));
            }
        }
    }
    return 0;
}

코드 설명

  1. fork()로 자식 프로세스를 생성합니다.
  2. 자식 프로세스는 ptrace(PTRACE_TRACEME, ...)를 통해 스스로 “나를 추적해도 좋아” 상태로 만듭니다.
  3. 자식 프로세스는 raise(SIGSTOP)으로 스스로 중단시킵니다. 이렇게 해야 부모 프로세스가 자식에게 ptrace 요청을 보낼 준비가 됩니다.
  4. 부모 프로세스는 waitpid(child, &status, 0)로 자식이 중단(SIGSTOP)될 때까지 기다립니다.
  5. 이후 부모 프로세스는 for 루프에서 계속 ptrace(PTRACE_CONT, child, NULL, NULL)로 자식을 재개시키고, 자식이 시그널 등의 이유로 중단될 때마다(또는 종료될 때까지) waitpid로 상태를 확인합니다.
  6. 이 예제를 컴파일하고 실행해 보면, 부모 프로세스가 자식을 중단시킨 뒤 계속 실행하면서, 자식의 종료 시점까지 추적함을 확인할 수 있습니다.

이벤트(시그널, 종료 등) 발생 시, 부모 프로세스가 이를 확인하고 실행할지 무시할지 결정할 수 있습니다. 위 예제에서는, 자식 프로세스가 SIGINT 시그널을 받을 때마다 부모 프로세스가 이를 확인하고, 이를 무시하고 있습니다. 고로 자식 프로세스가 죽지 않고 끝까지 실행되는 모습을 확인할 수 있습니다.

Parent: child has stopped, start tracing...
Child: i = 0
Parent: child stopped by signal 2
Child: i = 1
Parent: child stopped by signal 2
Child: i = 2
Parent: child stopped by signal 2
Child: i = 3
Parent: child stopped by signal 2
Child: i = 4
Parent: child stopped by signal 2
Parent: child exited with status 0

raise(SIGINT)대신 raise(SIGKILL) 등으로 시그널을 변경하면, 자식 프로세스가 종료될 수 있습니다. SIGKILL은 무조건적으로 프로세스를 종료시키는 시그널이므로, ptrace로도 제어할 수 없습니다.

Example 2: 메모리 읽기 및 쓰기

이제, ptrace의 더 강력한 기능인 메모리 읽기/쓰기를 활용해 보겠습니다.

메모리 읽기(PTRACE_PEEKTEXT, PTRACE_PEEKDATA)

  • addr 인자로 자식 프로세스의 가상 주소를 전달하면, 해당 주소의 워드(word) 단위를 읽어올 수 있습니다.
  • 일반적으로 텍스트(코드) 영역을 읽는 데 PTRACE_PEEKTEXT, 데이터(스택/힙) 영역을 읽는 데 PTRACE_PEEKDATA를 사용하지만, 내부 구현상 큰 차이는 없습니다.

메모리 쓰기(PTRACE_POKETEXT, PTRACE_POKEDATA)

  • 자식 프로세스의 메모리를 수정할 수 있습니다.
  • 주의할 점은, 코드를 수정하면 바로 세그먼트 오류(Segmentation Fault)가 발생할 수도 있고, 보안 측면에서도 매우 위험할 수 있으므로 신중하게 사용해야 합니다.

코드 예시

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#include <sys/ptrace.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/user.h>

char secret[20] = "HelloWorld";

int main() {
    pid_t child = fork();
    if (child == -1) {
        perror("fork");
        return 1;
    }

    if (child == 0) { // 자식 프로세스
        printf("Child: secret = %s / b(secret) = 0x%lx\n", secret, *(unsigned long*)secret);

        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        raise(SIGSTOP);

        printf("Child: secret after = %s / b(secret) = 0x%lx\n", secret, *(unsigned long*)secret);
        return 0;
    } else {
        // 부모 프로세스
        int status;
        waitpid(child, &status, 0);

        if (WIFSTOPPED(status)) { // 자식 프로세스의 memory를 peek/poke
            long word;
            unsigned long addr; 

            // secret 배열의 주소를 알기 위해서는
            // 실제 바이너리 분석 또는 고정된 오프셋 등을 활용해야 합니다.
            // 여기서는 예시를 위해 "임의의 주소를 이미 안다"고 가정합니다.
            // 따라서, fork 특성 상 같은 주소를 공유하는 전역변수를 생성해서 테스트 합니다.
            // 실제 상황에서는 symbol table, DWARF 디버그 정보 등을 사용해야 합니다.
            addr = (unsigned long)secret; // 가정: secret 배열의 시작 주소

            // 자식 프로세스의 secret 메모리를 읽고 쓰는 예시
            // 실제로는 이 주소가 맞지 않으면 세그먼트 오류가 발생할 수 있음
            errno = 0;
            word = ptrace(PTRACE_PEEKDATA, child, (void*)addr, NULL);

            // "Bye"라는 문자열을 쓰고 싶다고 가정
            // '\0', '\0', '\0', '\0', '\0', 'e', 'y', 'B'
            // 문자열을 리틀 엔디안으로 인코딩해서 long에 넣는 과정이 필요
            long newWord = 0x0000000000657942; 

            if (word == -1 && errno) {
                perror("ptrace peekdata");
            } else {
                printf("Parent: read word = 0x%lx / new word = 0x%lx\n", word, newWord);
            }

            if (ptrace(PTRACE_POKEDATA, child, (void*)addr, (void*)newWord) == -1) {
                perror("ptrace pokedata");
            }

            // 자식 프로세스 재개
            ptrace(PTRACE_CONT, child, NULL, NULL);
        }

        waitpid(child, &status, 0);
        if (WIFEXITED(status)) {
            printf("Parent: child exited.\n");
        }
    }
    return 0;
}

코드 설명

  1. fork()로 자식 프로세스를 생성합니다.
  2. 자식 프로세스는 secret 배열에 존재하는 문자열 (HelloWorld)을 출력합니다.
  3. 이후 raise(SIGSTOP)으로 스스로 중단시킵니다.
  4. 부모 프로세스는 waitpid(child, &status, 0)로 자식이 중단(SIGSTOP)될 때까지 기다립니다.
  5. 이후 부모 프로세스는 자식의 secret 배열의 값을 읽고 변경합니다.
  6. 이후 자식 프로세스를 재개시키고, 자식 프로세스가 종료될 때까지 대기합니다.
  7. 자식 프로세스는 바뀐 secret 배열의 값을 출력합니다.
  8. 이 예제를 실행해 보면, 다음과 같은 결과를 확인할 수 있습니다.

정상적으로 메모리를 읽고 수정하는 것을 확인할 수 있습니다.

Child: secret = HelloWorld / b(secret) = 0x726f576f6c6c6548
Parent: read word = 0x726f576f6c6c6548 / new word = 0x657942
Child: secret after = Bye / b(secret) = 0x657942
Parent: child exited.

실제로는 위 방법처럼 메모리 주소를 가져오기보단, DWARF 디버그 정보나 symbol table 등을 활용해야 합니다. 혹은 디버깅 기법을 활용해 메모리 주소를 찾아야 합니다.

Example 3: break point 설정하기

마지막으로, ptrace를 활용해 간단한 브레이크포인트를 설정해 보겠습니다. 브레이크포인트는 특정 메모리 주소에 SIGTRAP 시그널을 발생시켜 원하는 곳에서 프로세스를 중단시키는 기능입니다. 디버거를 만들기 위해 필요한 필수적인 기능 중 하나입니다.

레지스터 읽기/쓰기 (PTRACE_GETREGS, PTRACE_SETREGS)

  • struct user_regs_struct(x86-64 기준) 같은 구조체에 CPU 레지스터 값들이 담깁니다.
  • ptrace(PTRACE_GETREGS, child, NULL, &regs) 형태로 호출하면, 레지스터 값을 regs 구조체에 복사해 줍니다.
  • ptrace(PTRACE_SETREGS, child, NULL, &regs)로 레지스터를 원하는 값으로 변경할 수 있습니다.

개요

  1. 부모 프로세스가 자식 프로세스를 실행시키되, 특정 함수 시작 지점 등에 소프트웨어 브레이크포인트를 생성합니다. (일반적으로 x86-64에서 int 3(0xCC)를 삽입)
  2. 자식 프로세스가 해당 브레이크포인트에 도달하면 SIGTRAP 시그널로 중단됩니다.
  3. 부모 프로세스가 레지스터, 메모리 상태를 확인하거나 수정한 뒤, 다시 PTRACE_CONT 또는 PTRACE_SINGLESTEP를 통해 실행을 재개합니다.
    • PTRACE_CONT를 사용하면 다음 중단 지점까지 실행됩니다.
    • PTRACE_SINGLESTEP를 사용하면 한 명령어씩 실행됩니다.

소프트웨어 브레이크포인트 구현

  • 브레이크포인트를 설치할 위치의 원본 명령어를 읽어(PTRACE_PEEKTEXT), 1바이트를 0xCC(x86의 int 3)로 교체(PTRACE_POKETEXT)합니다.
  • 프로세스 실행이 해당 위치에 도달하면, SIGTRAP이 발생하면서 프로세스가 중단됩니다.
  • 부모 프로세스가 원하는 작업을 진행한 뒤, 원본 명령어로 복원하고, EIP/RIP 레지스터를 조정한 뒤, 실행을 재개합니다.

코드 예시

#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

#include <sys/ptrace.h>
#include <sys/wait.h>
#include <sys/user.h>

// 간단히, 자식이 실행할 함수
void test_function() {
    printf("Test Function Start\n");
    for (int i = 0; i < 3; ++i) {
        printf("In loop: i=%d\n", i);
        sleep(1);
    }
    printf("Test Function End\n");
}

int main() {
    pid_t child = fork();
    if (child == 0) { // 자식 프로세스
        ptrace(PTRACE_TRACEME, 0, NULL, NULL);
        raise(SIGSTOP);

        // test_function 실행
        test_function();
        test_function();

        return 0;
    } else { // 부모 프로세스
        int status;
        waitpid(child, &status, 0);

        // 브레이크포인트를 걸고자 하는 주소(이 예시에서는 test_function의 시작 주소)
        // 실제로는 심볼 테이블에서 주소를 구하거나, 동적 분석을 해야 합니다.
        unsigned long breakpoint_addr = (unsigned long)test_function;

        // 원본 명령어 저장용
        long original_data;

        // 1) 원본 명령어 읽기
        errno = 0;
        original_data = ptrace(PTRACE_PEEKTEXT, child, (void*)breakpoint_addr, NULL);
        if (original_data == -1 && errno) {
            perror("ptrace peektext");
            return 1;
        }

        // 2) 브레이크포인트(0xCC) 삽입
        long data_with_break = (original_data & 0xFFFFFFFFFFFFFF00) | 0xCC;
        // x86-64 리틀 엔디안에서, 첫 바이트가 0xCC가 되도록 수정
        if (ptrace(PTRACE_POKETEXT, child, (void*)breakpoint_addr, (void*)data_with_break) == -1) {
            perror("ptrace poketext");
            return 1;
        }

        // 자식 실행
        ptrace(PTRACE_CONT, child, NULL, NULL);

        for (; ;) {
            waitpid(child, &status, 0);
            if (WIFEXITED(status)) {
                printf("Child exited\n");
                break;
            }
            if (WIFSTOPPED(status)) {
                int sig = WSTOPSIG(status);
                if (sig == SIGTRAP) { // 브레이크 포인트 도달
                    printf("Hit breakpoint at 0x%lx\n", breakpoint_addr);

                    // 3) 브레이크포인트를 복원
                    // 원본 명령어 복원
                    ptrace(PTRACE_POKETEXT, child, (void*)breakpoint_addr, (void*)original_data);

                    // 4) RIP를 한 바이트 뒤로 되돌리기
                    // x86-64 환경에서 브레이크포인트 명령어(0xCC)가 1바이트이므로
                    // RIP에서 1만큼 빼서 원래 명령어 재실행
                    struct user_regs_struct regs;
                    ptrace(PTRACE_GETREGS, child, NULL, &regs);
                    regs.rip -= 1; // 브레이크포인트 명령어가 있는 위치
                    ptrace(PTRACE_SETREGS, child, NULL, &regs);

                    // 단일 스텝으로 명령어 한 번만 실행
                    ptrace(PTRACE_SINGLESTEP, child, NULL, NULL);
                    waitpid(child, &status, 0);

                    // 5) 다시 브레이크포인트 설치 (재진입 확인용)
                    ptrace(PTRACE_POKETEXT, child, (void*)breakpoint_addr, (void*)data_with_break);

                    // 이후 재개
                    ptrace(PTRACE_CONT, child, NULL, NULL);
                } else {
                    // 다른 시그널이면 계속
                    ptrace(PTRACE_CONT, child, NULL, sig);
                }
            }
        }
    }
    return 0;
}

코드 설명

  1. fork()로 자식 프로세스를 생성합니다.
  2. 자식 프로세스는 test_function을 총 2회 실행합니다.
  3. 부모 프로세스는 test_function 시작점에 브레이크포인트를 걸고, 자식 프로세스를 실행합니다.
  4. 자식 프로세스가 브레이크포인트에 도달하면, 변경된 명령어에 의해 SIGTRAP 시그널이 발생되어 중단됩니다.
  5. 부모 프로세스는 브레이크포인트를 복원하고, RIP 레지스터를 조정한 뒤, 다시 실행을 재개합니다.
  6. 이후 자식 프로세스가 종료될 때까지 이를 반복합니다.

정상적으로 브레이크 포인트를 만나 중단되는 것을 확인할 수 있습니다.

Hit breakpoint at 0x5baaac737269
Test Function Start
In loop: i=0
In loop: i=1
In loop: i=2
Test Function End
Hit breakpoint at 0x5baaac737269
Test Function Start
In loop: i=0
In loop: i=1
In loop: i=2
Test Function End
Child exited

실제로는 동적 분석을 통해 브레이크 포인트를 설정해야 합니다. 이 예시는 단순한 구현 예시입니다.

Conclusion

이상으로 ptrace를 활용한 간단한 디버깅 및 프로세스 제어 방법에 대해 알아보았습니다. ptrace는 프로세스의 내부 상태를 확인하고 수정하는 데 강력한 기능을 제공하므로, 디버깅 툴이나 보안 솔루션 등 다양한 분야에서 활용됩니다.

본 글에서 소개한 내용을 바탕으로 개인의 입맛에 맞는 디버거를 제작하거나, 유저의 코드를 제한하는 등 다양한 활용 방법이 존재합니다.