실시간으로 프로세스의 메모리를 추적하는 툴을 제작을 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% 정도 완성이 된 것 같다.
프로그램 동작 과정은 이렇다.
- brk, mmap, munmap, page fault 발생 시 netlink-socket을 사용해서 유저 프로세스로 PID를 보낸다.
- 유저는 해당 PID를 받고, /proc/PID디렉토리로 들어가, status 파일에서 VmRSS와 VmSize를 가져온다.
- 계속 반복
유저단에서 커널 이벤트를 연속적으로 받기 때문에
/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 |
