메모리 할당과 해제에 관련된 시스템콜인 brk()
, mmap()
, munmap()
의 시스템콜 넘버를 리눅스 커널 소스에서 확인했었다.
이번엔, 어떤 프로세스가 실행되면서 시스템콜을 발생시킨다면 어떤 시스템콜을 발생시키는지
PTRACE_SYSCALL을 활용해서 확인해보겠다.
리눅스에서 프로세스를 추적하려면, ptrace()
함수를 사용한다.
ptrace의 함수 사용법을 알기 위해서, man 2 ptrace로 매뉴얼을 확인하고(간단한 문장은 직접 해석하고,
문장이 길어지면 GPT에서 부탁한다),
깃헙에서 리눅스 커널 소스를 뒤져보았다.(커널 소스를 100%이해하지는 못하지만 보는 것만으로도 실력이 올라가는 느낌..)
PTRACE_ATTACH
Attach to the process specified in pid, making it a tracee of the
calling process. The tracee is sent a SIGSTOP ~
매뉴얼에 나와있는 설명이다.
간단하게 말하자면, “추적대상프로세스(tracee)가 생기고 이 함수를 호출한 프로세스(tracer)가 추적할 수 있게 한다.” 라는 뜻.
코드를 봐보자
void ptrace_attach_process(pid_t pid)
{
ptrace(PTRACE_ATTACH, pid, NULL, NULL);
waitpid(pid, &status, 0);
printf("Attach to PID: %d\n", pid);
}
추적하고자하는 pid에 PTRACE_ATTACH를 했고, waitpid로 해당 pid(추적자)가 중지하길 기다린다.
그러면 왜 중지하는가??
다시 man 2 ptrace의 매뉴얼에서 PTRACE_ATTACH의 나머지 부분을 봐보자.
The tracee is sent a SIGSTOP, but will not neces\u2010
sarily have stopped by the completion of this call; use waitpid(2) to
wait for the tracee to stop.
커널이 해당 pid에게 SIGSTOP시그널을 보내고, 이 pid는 프로세스를 멈춘다.
멈추면 waitpid()에서 상태변화를 감지해서 부모 프로세스가 마저 실행된다.
예를 들자면, 만약 부모가 PID1이고 자식이 PID2라고 가정했을때,
PID2는 추적자가되고 waitpid(PID2)로 PID2의 상태변화가 있을때까지, PID1이 블로킹상태가 된다.
만약 PID2가 중지되어 상태변화가 생긴다면, PID1에게 알리고 PID1이 마저 실행한다.
PTRACE_GETREGSET
시스템콜 번호를 저번에 알게되었다.
만약, 프로세스A에서 실행하다가 시스템콜이 발생하면, 시스템콜 넘버를 레지스터에 기록을 할 것이다.
그 레지스터의 값을 가져오면 어떤 시스템콜을 호출했는지 볼 수 있다.
PTRACE_GETREGSET으로 특정 레지스터에서 값을 가져와보자.
그러기 위해서, 또다시 매뉴얼에서 PTRACE_GETREGSET을 확인한다.(글에서는 생략한다)
void get_syscall(pid_t pid)
{
struct user_pt_regs regs;
struct iovec io = {
.iov_base = ®s,
.iov_len = sizeof(regs)
};
ptrace(PTRACE_GETREGSET, pid, (void *)NT_PRSTATUS, &io);
printf("Current system call number: %llu x0: %llu, x1: %llu\n", regs.regs[8], regs.regs[0], regs.regs[1]);
}
iovec
와, user_pt_regs
는 이미 커널에 정의된 구조체이다.
struct user_pt_regs {
__u64 regs[31];
__u64 sp;
__u64 pc;
__u64 pstate;
};
struct iovec
{
void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */
__kernel_size_t iov_len; /* Must be size_t (1003.1g) */
};
ptrace매뉴얼에서는, GETREGSET으로 레지스터 값을 읽어올 때,
iovec구조체를 통해서 레지스터 값을 읽어오라고 하고있다.
그러면 시스템콜 넘버는 어떤 레지스터에 저장되는가??
8번 레지스터
에 저장된다.(aarch64 한정)
즉, 시스템콜이 호출되면 커널은 시스템콜 넘버를 특정한 레지스터에 저장하는데,
aarch64에서는 그 레지스터가 바로 x8이다.
따라서 PTRACE_GETREGSET을 통해 x8값을 읽어내면 어떤 시스템콜이 호출된건지 알 수 있다.
PTRACE_SYSCALL
오케이, 그러면 지금까지 ptrace()
를 이용해서 추적자를 만들었고.
8번 레지스터에서 시스템 콜 넘버를 가져오는 것 까지 했다.
이제는,
시스템 콜이 발생한 정확한 타이밍(시스템콜 진입시점, 빠져나오는 시점)에 레지스터의 값을 읽을 수 있도록 해야한다.
man 2 ptrace에 따르면
Restart the stopped tracee as for PTRACE_CONT, but arrange for the
tracee to be stopped at the next entry to or exit from a system call,
“시스템콜 진입시점에 멈추고, 시스템콜 종료 시점에 멈춘다”라고 적혀있다.
그렇다면, 대략 생각되는 흐름은 이렇다.
- system call 진입시에 멈춘다.
- 부모에서 system call 넘버를 레지스터에서 읽는다.
- system call 빠져나올때 다시 멈춘다.
- 부모에서 마저 실행한다.(다음 시스템콜을 확인한다)
이 과정을 코드로 나타내보면,
void ptrace_systemcall(pid_t pid)
{
while(WIFSTOPPED(status))
{
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
waitpid(pid, &status, 0);
if(WIFEXITED(status))
break;
get_syscall(pid); //멈춘 상태에서 부모가 레지스터에서 시스템콜 넘버 읽는다.
ptrace(PTRACE_SYSCALL, pid, NULL, NULL);
waitpid(pid, &status, 0); //자식이 마저 끝낼때까지 기다린다.
if(WIFEXITED(status))
break;
}
}
❓왜 시스템콜 탈출 시점에서도 멈출까?
- 진입시점에서 멈춤 → 레지스터값을 읽기 위해서
- 탈출시점에서 멈충 → 시스템콜 리턴값을 알기 위해서
결과
간단하게 malloc으로 동적메모리를 할당하는 프로그램을 작성했다.
그리고 malloc을 실행했을때, 어떤 시스템콜 넘버가 나오는지 확인해보자

214번 시스템콜 넘버가 호출 된 것을 볼수있다.
저번에 확인을 했듯이, aarch64에서 214번 시스템콜은 brk()시스템콜
이다.
즉 malloc을 하면, brk()가 실행된다 라는 것을 알 수 있다.
'C프로그래밍 > MemoryTracker' 카테고리의 다른 글
RealtimeMemTracker - 3, 시스템콜 번호 확인하기 (1) | 2025.03.23 |
---|---|
RealtimeMemTracker - 2, 현재 실행중인 프로세스 확인 (0) | 2025.03.23 |
RealtimeMemTracker - 1, 다른 프로세스의 SystemCall 추적하기 (0) | 2025.03.20 |