메모리 프로파일러 (시스템 콜 후킹)

2025. 10. 5. 18:12·Linux

실시간으로 프로세스의 메모리를 추적하는 툴을 제작을 25년 2월부터 시작했다.

처음에는 ptrace를 활용해서 추적하려했으나 생각하는대로 동작하지 않았고

ptrace 동작방식 자체가 해당 프로세스를 중간중간 멈추게 하기 때문에 실시간성이 없었다...

 

그렇게 지지부진한 상태로 있다가,

커널 공부하는 김에 메모리 관련 시스템콜이 발생하면 유저 공간으로 전달해주도록 수정하면

실시간성이 확보되지않을까 싶어서 이 방식으로 다시 시작했다.

 

또, 커널 ↔ 유저 데이터 전달이 이뤄져야하기 때문에

어떻게 하지? 생각하다가 Netlink-Socket이 있다는걸 알았고 사용했다.

 

커널에 netlink-socket 내부 모듈을 작성했다.

https://c-pointers.com/lsp/Netlink/Netlink/Basic_example/Basic_example.html

 

Basic example netlink — C Pointers

What is the purpose of the socket(AF_NETLINK, SOCK_RAW, NETLINK_TESTFAMILY) call? It creates a raw Netlink socket with a custom family identifier (NETLINK_TESTFAMILY).

c-pointers.com

이 예제를 많이 참고해서

 

아래와 같은 코드를 만들었다.

/*
 *	Written By Jin Minu
 */

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/netlink.h>
#include <asm/types.h>
#include <net/net_namespace.h>
#include <net/sock.h>
#include <linux/pid.h>
#include <linux/string.h>

#define NETLINK_JMW 30
#define SYSCALL_NAME_LENGTH 10

MODULE_LICENSE("GPL");
MODULE_AUTHOR("JMW");

struct sock *netlink_socket = NULL;
static pid_t target_pid = 0;

typedef struct syscall_data {
	pid_t pid;
	char name[SYSCALL_NAME_LENGTH]; // system call name
} SYSCALL_DATA;

/*
 *	메세지 전송 : Kernel to User
 *	다음의 순서를 따른다.
 *	
 *	1. 버퍼 할당
 *	2. 헤더 추가
 *	3. 페이로드 복사
 *	4. 컨트롤 블록 설정
 *	5. 전송
 */
void nl_send_msg(pid_t pid, const char *syscall_name)
{
	SYSCALL_DATA data;
	struct sk_buff *skb_out;
	struct nlmsghdr *nlh;

	data.pid = pid;
	strncpy(data.name, syscall_name, SYSCALL_NAME_LENGTH - 1); // 'b' 'r' 'k' '\0' '\0' '\0' '\0' '\0' '\0'
	data.name[SYSCALL_NAME_LENGTH - 1] = '\0'; // 예외의 경우

	int data_length = sizeof(data);

	/*
	 * nlmsg_new : Allocate a new netlink message buffer
	 *
	 * data_length : payload
	 * GFP_KERNEL : type of memory to allocate
	 */
	skb_out = nlmsg_new(data_length, GFP_KERNEL); 
	if (!skb_out) {
		printk(KERN_ERR "[JMW] Netlink Alloc failed!\n");
		return;
	}

	/*
	 * nlmsg_put : add new netlink message to an skb (skb : socket buffer)
	 *
	 * skb_out : socket buffer to store message in
	 * port id : sending process Port ID (보내려는 프로세스의 pid)
	 * seq : sequence number
	 * NETLINK_JMW : message type
	 * data_length : length of payload
	 * flag : 
	 */	
	nlh = nlmsg_put(skb_out, 0, 0, NETLINK_JMW, data_length, 0);
	if (!nlh) {
		kfree_skb(skb_out);
		return;
	}

	/*
	 * nlmsg_data : payload의 포인터
	 * nlmsg_data의 인자 nlh : netlink message header
	 */
	memcpy(nlmsg_data(nlh), &data, data_length); // payload 포인터에 data_length만큼 data copy

	// NETLINK_CB(skb_out).dst_group = 0;

	/*
	 * nlmsg_unicast : 특정 pid와 1:1통신
	 * netlink_socket : 메세지 보내는 데 사용할 커널 Netlink Socket pointer
	 * skb_out : message buffer (헤더랑 payload 담김)
	 * monitor_pid : 대상 프로세스 PID -> 도착 소켓의 Port ID
	 */

	if (target_pid == 0) { // 아직 수신자 등록 안되었을 때
		return;
	}

	int ret = nlmsg_unicast(netlink_socket, skb_out, target_pid); // unicast : 1:1통신
	if (ret < 0) {
		printk(KERN_ERR "[JMW] Netlink send error!\n");
	}
}

EXPORT_SYMBOL(nl_send_msg);

static void nl_recv_msg(struct sk_buff *skb) //인자로 메세지 버퍼 받음
{
	/*
	 * monitor_pid(결과를 보낼 pid)를 설정
	 */
	struct nlmsghdr *nlh;
	pid_t sender_pid;

	if (!skb)
		return;

	nlh = nlmsg_hdr(skb); // message buffer pointer
	sender_pid = nlh->nlmsg_pid; // get sender's pid

	if (sender_pid != 0) {
		target_pid = sender_pid;
		printk(KERN_INFO "[JMW] Monitor pid is : %d\n", target_pid);
	}
	else {
		printk(KERN_WARNING "[JMW] sender_pid = 0");
	}
}

/*
 * 	모듈 초기화 / 종료
 */
static int __init netlink_init(void)
{
	struct netlink_kernel_cfg config = {
		.input = nl_recv_msg,
	};

	netlink_socket = netlink_kernel_create(&init_net, NETLINK_JMW, &config);
	if (!netlink_socket) {
		printk(KERN_INFO "[JMW] Netlink Socket creation failed!\n");
		return -ENOMEM;	
	}

	printk(KERN_INFO "[JMW] Netlink Socket creation success!\n");

	return 0;
}

static void __exit netlink_exit(void)
{
	if (netlink_socket) {
		netlink_kernel_release(netlink_socket);
	}
	printk(KERN_INFO "[JMW] Netlink Module unloaded!\n");
}

module_init(netlink_init);
module_exit(netlink_exit);

 

소켓 프로그래밍과 상당히 유사한데

시스템 프로그램을 하면서 잠깐 해본거라 기억은 잘 안나지만..

다시 책을 뒤져가며 공부했다.

 

초기 프로토타입만 빠르게 작성하고

리눅스를 빌드하여, 위 시스템콜 이벤트가 발생했을때 유저단으로 알림이 오는지만 확인했는데

잘 동작했다. (Vim으로 작성해서 작업속도가 좀 느림...)

 

그리고 커널API같은건 자료가 많이 없어서,

Linux man page, Bootlin같은 사이트에서 함수 사용법을 찾가아면서 개발했는데 그래서 시간이 매우매우 오래걸렸다.


현재 90% 정도 완성이 된 것 같다.

프로그램 동작 과정은 이렇다.

  1. brk, mmap, munmap, page fault 발생 시 netlink-socket을 사용해서 유저 프로세스로 PID를 보낸다.
  2. 유저는 해당 PID를 받고, /proc/PID디렉토리로 들어가, status 파일에서 VmRSS와 VmSize를 가져온다.
  3. 계속 반복

유저단에서 커널 이벤트를 연속적으로 받기 때문에

/proc/[PID] 디렉토리로 들어가서 탐색할 때 실시간성 확보하려고

멀티 프로세스로 바꿨다.

 

프로세스1에서는 커널에서 전달받은 PID와 시스템콜 이름을 프로세스2에게 전달,

프로세스2에서는 /proc디렉토리 탐색, 로그 출력 및 그래프를 그린다.

이 과정에서 IPC프로그래밍이 조금 들어갔다.(pipe, shared memory)

 

그리고 코드를 작성하면서 커널의 파일분할, 소스 방식들을 많이 참고했다.

static inline같은..

 

로그파일도 작성되는 기능을 만들었는데

ftrace를 흉내 내보고싶었다.


프로젝트를 하면서

커널수정 -> 빌드 과정에서 상당한 시간이 소요되었다.. (라즈베리파이 자체가 느림)

다음부터는 메인 컴퓨터에서 크로스컴파일을 진행하는게 더 작업이 빠르게 진행될듯하다.

 

또한 커널 수정은 항상 위험하다.

예를 들어, brk 시스템콜을 수정, 커널 빌드 과정에서 커널패닉이 발생했는데,

부팅중에도 brk가 호출된다는 사실을 간과했었다.

따라서 부팅중에 netlink socket이 제대로 초기화되어있지 않다면, 문제가 발생한다.

 

Makefile도 직접 작성했는데, 확실히 IDE만 사용하다가

빌드 스크립트도 작성하려고하니 귀찮고 시간이 오래걸린다.

 

또한 커널에서 strcpy같은 함수를 사용할 때 주의해야한다는점 .. 등등

strncpy같은 안전한 함수를 사용하자..!

 

프로젝트의 단점이라고 한다면,

직접 커널 소스를 수정해야 한다는 것인데, Kprobe, eBPF같은 기술을 활용해보는 것도 좋은 방법이겠다.


https://github.com/Jminu/Memory-Tracking

 

GitHub - Jminu/Memory-Tracking: arm64 Linux 6.1v System Call hooking and Memory Tracking using Netlink-Socket

arm64 Linux 6.1v System Call hooking and Memory Tracking using Netlink-Socket - Jminu/Memory-Tracking

github.com

실행 화면
로그 파일

'Linux' 카테고리의 다른 글

첫 리눅스 커널 기여  (4) 2025.12.22
프로세스 생성 분석 fork (thread_info, task_struct 구조체) - 3  (2) 2025.11.20
프로세스 생성 분석 fork (태스크 복사 및 초기화 부분) - 2  (0) 2025.09.30
프로세스 생성 분석 fork(콜 스택) - 1  (4) 2025.09.28
커널에서 static inline 함수선언과 wrapping  (0) 2025.09.20
'Linux' 카테고리의 다른 글
  • 첫 리눅스 커널 기여
  • 프로세스 생성 분석 fork (thread_info, task_struct 구조체) - 3
  • 프로세스 생성 분석 fork (태스크 복사 및 초기화 부분) - 2
  • 프로세스 생성 분석 fork(콜 스택) - 1
Jminu
Jminu
  • Jminu
    뇌 구조가 바이너리
    Jminu
  • 전체
    오늘
    어제
    • 분류 전체보기
      • C프로그래밍
        • 오류해결
        • 개인 공부
        • Programming Lab(학교수업)
        • MemoryTracker
      • C++
        • 개인 공부
      • 자료구조(Data Structure)
      • ARM arch
        • Cortex-M
        • FreeRTOS
      • 컴퓨터 공학(Computer Science)
        • OS
        • 컴퓨터 구조
      • Qualcomm 기업과제
      • Linux
      • Web
      • 똥글
      • 백준
      • Git 학습
        • 오류해결
        • 학습중
      • Python
        • 오류해결
        • 개인 공부
  • 블로그 메뉴

    • 태그
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    순환
    백준
    버퍼
    Branch
    시스템콜
    피보나치
    Git
    토발즈
    commit
    C++
    파이썬
    커널
    이진 트리
    c언어
    드라이버 분석
    앤드류모튼
    커널 기여
    리눅스
    동적메모리
    포인터
    파일 입출력
    자료구조
    스택
    rubikpi3
    arm
    yolo
    rubik pi
    INIT
    Qualcomm
    소수
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
Jminu
메모리 프로파일러 (시스템 콜 후킹)
상단으로

티스토리툴바