Linux内核trace_sched_process_exec钩子注册示例

Linux内核trace_sched_process_exec钩子注册示例

sched_process_exec是一个Linux内核跟踪点(tracepoint),用于在进程执行execve()系统调用时触发。以下是如何注册和使用这个tracepoint的示例。

1. 使用tracepoint_notifier机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/trace_events.h>
#include <linux/sched.h>

static void tp_sched_process_exec(void *ignore,
struct task_struct *task,
pid_t old_pid,
struct linux_binprm *bprm)
{
char filename[256];

// 获取可执行文件路径
char *pathname = d_path(&bprm->file->f_path, filename, sizeof(filename));

printk(KERN_INFO "exec: pid=%d (%s -> %s) path=%s\n",
task->pid, task->comm, bprm->filename, pathname);
}

static int __init trace_init(void)
{
int ret;

// 注册tracepoint通知
ret = register_trace_sched_process_exec(tp_sched_process_exec, NULL);
if (ret) {
printk(KERN_ERR "Failed to register tracepoint\n");
return ret;
}

printk(KERN_INFO "sched_process_exec tracepoint registered\n");
return 0;
}

static void __exit trace_exit(void)
{
// 注销tracepoint通知
unregister_trace_sched_process_exec(tp_sched_process_exec, NULL);

printk(KERN_INFO "sched_process_exec tracepoint unregistered\n");
}

module_init(trace_init);
module_exit(trace_exit);
MODULE_LICENSE("GPL");

2. 使用ftrace事件机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/trace_events.h>

static struct trace_event_call *event_call;

static notrace void probe_sched_process_exec(void *ignore,
struct task_struct *task,
pid_t old_pid,
struct linux_binprm *bprm)
{
char filename[256];
char *pathname = d_path(&bprm->file->f_path, filename, sizeof(filename));

trace_printk("exec: pid=%d (%s -> %s) path=%s\n",
task->pid, task->comm, bprm->filename, pathname);
}

static int __init trace_init(void)
{
int ret;

// 获取sched_process_exec事件
event_call = trace_get_event_call("sched", "sched_process_exec");
if (!event_call) {
printk(KERN_ERR "Failed to get sched_process_exec event\n");
return -ENODEV;
}

// 注册probe
ret = trace_event_probe_register(event_call, probe_sched_process_exec, NULL);
if (ret) {
printk(KERN_ERR "Failed to register probe\n");
return ret;
}

printk(KERN_INFO "sched_process_exec probe registered\n");
return 0;
}

static void __exit trace_exit(void)
{
// 注销probe
if (event_call)
trace_event_probe_unregister(event_call, probe_sched_process_exec, NULL);

printk(KERN_INFO "sched_process_exec probe unregistered\n");
}

module_init(trace_init);
module_exit(trace_exit);
MODULE_LICENSE("GPL");

3. 使用BPF (eBPF) 方式

如果你使用的是Linux 4.7+内核,可以使用BPF来跟踪这个事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <linux/bpf.h>
#include "bpf_helpers.h"

SEC("tracepoint/sched/sched_process_exec")
int bpf_prog(struct trace_event_raw_sched_process_exec *ctx)
{
// 这里可以使用BPF辅助函数记录数据
// 例如通过perf事件或maps输出

return 0;
}

char _license[] SEC("license") = "GPL";

注意事项

  1. 需要确保内核配置了CONFIG_TRACEPOINTSCONFIG_SCHED_TRACER
  2. 在生产环境中使用时,建议使用更高效的记录方式(如perf事件)而非printk
  3. 对于BPF方式,需要内核支持BPF并加载BPF程序
  4. 获取文件路径时要注意内核上下文限制

以上示例展示了不同方式注册sched_process_exec tracepoint的方法,你可以根据具体需求选择适合的方式。

linux内核中trace_sched_process_exec中获取进程的启动参数的方法,要求不能有睡眠

在Linux内核的trace_sched_process_exec跟踪点中获取进程启动参数需要谨慎处理,因为不能访问用户空间内存(可能引起睡眠/缺页中断),且参数可能分布在多个内存页中。以下是一个安全获取进程启动参数的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <linux/trace_events.h>
#include <linux/binfmts.h>
#include <linux/string.h>

#define MAX_ARG_LEN 128 // 限制参数长度防止溢出

static void tp_sched_process_exec(void *ignore,
struct task_struct *task,
pid_t old_pid,
struct linux_binprm *bprm)
{
char arg_buf[MAX_ARG_LEN];
int argc = bprm->argc;
int i, offset = 0;

printk(KERN_INFO "exec: pid=%d (%s) argc=%d\n",
task->pid, bprm->filename, argc);

// 获取参数指针数组 - 注意这是用户空间指针!
unsigned long __user *argv_user = (unsigned long __user *)bprm->p;

for (i = 0; i < argc && offset < MAX_ARG_LEN - 1; i++) {
char __user *argp;
unsigned long arg_addr;

// 1. 获取用户空间参数地址
if (copy_from_user(&arg_addr, &argv_user[i], sizeof(arg_addr)))
break;

argp = (char __user *)arg_addr;
if (!argp) break;

// 2. 安全复制单个参数
int len = strnlen_user(argp, MAX_ARG_LEN - offset - 1);
if (len <= 0 || len > MAX_ARG_LEN - offset - 1) break;

// 3. 实际复制操作
if (copy_from_user(arg_buf + offset, argp, len)) break;

offset += len;
arg_buf[offset++] = ' '; // 参数分隔符
}

if (offset > 0) {
arg_buf[offset > 0 ? offset - 1 : 0] = '\0'; // 替换最后的空格
printk(KERN_INFO "argv: %s\n", arg_buf);
}
}

关键点说明:

  1. 安全访问用户空间

    • 使用copy_from_user()strnlen_user()避免直接访问用户空间
    • 这些函数处理了可能发生的页面错误
  2. 参数长度限制

    • 使用strnlen_user()限制最大长度
    • MAX_ARG_LEN防止缓冲区溢出
  3. 参数位置计算

    • bprm->p包含用户空间argv数组地址
    • bprm->argc提供参数数量
  4. 内存安全

    • 每次迭代检查剩余缓冲区空间
    • 处理可能的复制失败

替代方案(eBPF):

对于更新内核(4.17+),使用eBPF更安全高效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct linux_binprm *bprm = (struct linux_binprm *)ctx->bprm;
char args[256];
char *p = args;
int i, max = sizeof(args) - 1;

for (i = 0; i < bprm->argc && max > 0; i++) {
unsigned long arg_addr;
bpf_probe_read_user(&arg_addr, sizeof(arg_addr),
bprm->p + i * sizeof(unsigned long));

int len = bpf_probe_read_user_str(p, max, (char *)arg_addr);
if (len <= 0) break;

p += len - 1; // 去掉结尾的null
*p++ = ' ';
max -= len;
}

if (p > args) *(p-1) = 0; // 终止字符串

bpf_trace_printk("exec: %s %s\n", ctx->filename, args);
return 0;
}

注意事项:

  1. 内核版本差异

    • 不同内核版本中linux_binprm结构可能有变化
    • 需要验证bprm->pbprm->argc的可用性
  2. 性能考虑

    • 限制参数总长度(建议256-512字节)
    • 避免在频繁执行的路径中打印完整参数
  3. 安全边界

    • 始终检查返回值
    • 处理恶意程序提供的非法指针
  4. 生产环境建议

    • 优先使用eBPF方案
    • 或通过perf events输出数据
    • 避免在内核日志中记录敏感信息

使用这些方法可以在不睡眠的前提下安全地获取execve的参数,但需要注意参数截断的可能性(特别是长参数列表的情况)。

linux 5.10内核中trace_sched_process_exec中获取进程的启动参数的方法,要求不能有睡眠

在 Linux 5.10 内核中,从 trace_sched_process_exec 获取进程启动参数而不引起睡眠需要谨慎处理用户空间内存访问。以下是安全的方法:

安全获取启动参数的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <linux/trace_events.h>
#include <linux/binfmts.h>
#include <linux/uaccess.h>
#include <linux/string.h>

#define MAX_ARG_STRLEN 128 // 单个参数最大长度
#define MAX_ARG_COUNT 32 // 最大参数数量

static void tp_sched_process_exec(void *ignore,
struct task_struct *task,
pid_t old_pid,
struct linux_binprm *bprm)
{
unsigned long argv_user[MAX_ARG_COUNT];
char arg_buf[MAX_ARG_STRLEN];
char args_flat[MAX_ARG_STRLEN * 2]; // 用于拼接参数
int i, offset = 0;
int arg_count = min(bprm->argc, (int)MAX_ARG_COUNT);

// 1. 获取用户空间参数指针数组
if (copy_from_kernel_nofault(argv_user, bprm->p,
sizeof(unsigned long) * arg_count)) {
trace_printk("Failed to copy argv pointers\n");
return;
}

args_flat[0] = '\0';

// 2. 逐个复制参数
for (i = 0; i < arg_count && offset < sizeof(args_flat) - MAX_ARG_STRLEN; i++) {
unsigned long arg_addr = argv_user[i];
int len;

// 安全获取字符串长度
len = strnlen_user_nofault((const char __user *)arg_addr, MAX_ARG_STRLEN);
if (len <= 0 || len > MAX_ARG_STRLEN) continue;

// 安全复制字符串内容
if (copy_from_user_nofault(arg_buf, (const char __user *)arg_addr, len))
continue;

// 确保以null结尾
arg_buf[len - 1] = '\0';

// 拼接参数
scnprintf(args_flat + offset, sizeof(args_flat) - offset,
"%s%s", (i > 0) ? " " : "", arg_buf);
offset += strlen(args_flat + offset);
}

// 3. 输出结果 - 实际使用中建议使用tracepoint输出而非printk
trace_printk("exec: pid=%d cmd=%s args=%s\n",
task->pid, bprm->filename, args_flat);
}

关键安全措施

  1. 使用无睡眠的访问函数

    • copy_from_kernel_nofault():用于访问内核空间数据
    • copy_from_user_nofault():用于访问用户空间数据
    • strnlen_user_nofault():安全获取用户空间字符串长度
  2. 缓冲区边界保护

    • 限制最大参数数量(MAX_ARG_COUNT)
    • 限制单个参数长度(MAX_ARG_STRLEN)
    • 使用scnprintf()防止缓冲区溢出
  3. 错误处理

    • 检查所有内存访问函数的返回值
    • 跳过无效参数而非中止处理
  4. 内存安全

    • 使用栈上固定大小的缓冲区
    • 确保字符串正确终止

Linux 5.10 特定实现细节

  1. linux_binprm 结构变化

    1
    2
    3
    4
    5
    6
    7
    struct linux_binprm {
    // ...
    int argc, envc; // 参数和环境变量计数
    const char *filename; // 可执行文件名
    unsigned long p; // 用户空间参数指针数组地址
    // ...
    };
  2. 无睡眠访问函数

    1
    2
    3
    4
    5
    // 从用户空间复制数据 (不会睡眠)
    long copy_from_user_nofault(void *dst, const void __user *src, size_t size);

    // 获取用户空间字符串长度 (不会睡眠)
    long strnlen_user_nofault(const void __user *str, long len);

替代方案:使用 eBPF (推荐)

对于生产环境,推荐使用 eBPF 方案,更安全高效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct linux_binprm *bprm = (struct linux_binprm *)ctx->bprm;
unsigned long argv[MAX_ARGS];
char arg_buf[MAX_ARG_LEN];
char args_flat[MAX_TOTAL_LEN];
int i, offset = 0;

// 1. 复制参数指针数组
bpf_probe_read_kernel(&argv, sizeof(argv), bprm->p);

// 2. 处理每个参数
for (i = 0; i < min(bprm->argc, MAX_ARGS); i++) {
if (offset >= MAX_TOTAL_LEN - MAX_ARG_LEN) break;

// 复制单个参数
int len = bpf_probe_read_user_str(arg_buf, sizeof(arg_buf),
(const char *)argv[i]);
if (len <= 0) continue;

// 拼接参数
if (i > 0) {
bpf_snprintf(args_flat + offset, MAX_TOTAL_LEN - offset, " ");
offset++;
}

bpf_snprintf(args_flat + offset, MAX_TOTAL_LEN - offset, "%s", arg_buf);
offset += len - 1; // 减去末尾的null
}

// 3. 输出到perf缓冲区
bpf_trace_printk("exec: %s %s", ctx->filename, args_flat);
return 0;
}

注意事项

  1. 上下文约束

    • trace_sched_process_exec 在 execve 系统调用上下文中运行
    • 禁止任何可能睡眠的操作(如分配内存、等待锁等)
  2. 性能影响

    • 限制处理的参数数量和长度
    • 避免复杂字符串处理
    • 生产环境中建议使用 eBPF 的 perf 环形缓冲区
  3. 安全边界

    1
    2
    3
    // 防止恶意参数导致缓冲区溢出
    #define MAX_ARG_STRLEN (PAGE_SIZE / 4) // 通常为1024
    #define MAX_ARG_COUNT 64
  4. 内核版本适配

    • 不同内核版本中结构体可能变化
    • 5.10 中 linux_binprm 包含 argcp 成员
    • 使用 #ifdef 处理版本差异

这种方法在 Linux 5.10 内核中安全地获取 execve 参数,同时遵守原子上下文的约束,避免了任何可能导致睡眠的操作。

Linux内核支持copy_from_user_nofault函数么

在 Linux 内核中,没有名为 copy_from_user_nofault 的标准函数。这是一个常见的误解,我需要澄清这一点并为您提供正确的替代方案。

正确的无睡眠用户空间访问函数

在 Linux 5.10 内核中,用于原子上下文的用户空间访问函数是:

  1. copy_from_kernel_nofault()
    用于从内核空间安全读取数据

  2. copy_from_user_nofault() 不存在
    正确的函数是:

    1
    2
    long probe_kernel_read(void *dst, const void *src, size_t size);
    long probe_user_read(void *dst, const void __user *src, size_t size);

    在 Linux 5.10 中,这些函数被定义为:

    1
    2
    3
    4
    5
    // 访问用户空间
    long probe_user_read(void *dst, const void __user *src, size_t size);

    // 访问内核空间
    long probe_kernel_read(void *dst, const void *src, size_t size);

实际使用示例 (Linux 5.10)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <linux/uaccess.h>
#include <linux/string.h>

#define MAX_ARG_LEN 128

static void tp_sched_process_exec(void *ignore,
struct task_struct *task,
pid_t old_pid,
struct linux_binprm *bprm)
{
unsigned long argv_user[16];
char arg_buf[MAX_ARG_LEN];
char args_flat[512];
int i, offset = 0;
int argc = min(bprm->argc, 16);

// 1. 复制参数指针数组 (内核空间到内核空间)
if (probe_kernel_read(argv_user, bprm->p, sizeof(unsigned long) * argc)) {
trace_printk("Failed to copy argv pointers\n");
return;
}

args_flat[0] = '\0';

// 2. 逐个复制参数 (用户空间到内核空间)
for (i = 0; i < argc && offset < sizeof(args_flat) - MAX_ARG_LEN; i++) {
unsigned long arg_addr = argv_user[i];
long len;

// 安全读取用户空间字符串
len = probe_user_read(arg_buf, (const void __user *)arg_addr, MAX_ARG_LEN);
if (len <= 0) continue;

// 确保不越界
if (len > MAX_ARG_LEN) len = MAX_ARG_LEN;

// 添加空格分隔符
if (offset > 0 && offset < sizeof(args_flat) - 1) {
args_flat[offset++] = ' ';
}

// 复制参数内容
memcpy(args_flat + offset, arg_buf, len);
offset += len;

// 确保null终止
if (offset < sizeof(args_flat)) {
args_flat[offset] = '\0';
}
}

trace_printk("exec: pid=%d cmd=%s args=%s\n",
task->pid, bprm->filename, args_flat);
}

关键函数说明 (Linux 5.10)

函数 用途 是否可能睡眠
probe_user_read() 从用户空间读取数据 不会睡眠
probe_kernel_read() 从内核空间读取数据 不会睡眠
strncpy_from_user_nofault() 安全复制用户空间字符串 不会睡眠

特别说明:strncpy_from_user_nofault

对于字符串操作,更优的选择是使用:

1
long strncpy_from_user_nofault(char *dst, const void __user *src, long count);

使用示例:

1
2
3
4
5
6
len = strncpy_from_user_nofault(arg_buf, (const void __user *)arg_addr, MAX_ARG_LEN);
if (len < 0) {
// 处理错误
} else if (len == MAX_ARG_LEN) {
arg_buf[MAX_ARG_LEN - 1] = '\0'; // 确保终止
}

历史变化

  1. Linux 4.11+:

    1
    2
    probe_user_read()
    probe_kernel_read()
  2. Linux 5.8+:
    引入了更一致的API:

    1
    2
    copy_from_user_nofault() // 实际存在,但有限制
    copy_to_user_nofault()

    但在5.10中仍建议使用probe_*系列函数

  3. Linux 5.14+:
    最终统一为:

    1
    2
    copy_from_user_nofault()
    copy_to_user_nofault()

生产环境建议

对于实际使用,特别是需要高可靠性的场景:

  1. 优先使用 eBPF:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    SEC("tracepoint/sched/sched_process_exec")
    int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
    {
    struct linux_binprm *bprm = (void *)ctx->bprm;
    char args[256];
    // 使用BPF辅助函数安全读取
    bpf_probe_read_user_str(args, sizeof(args), (void *)bprm->p);
    bpf_printk("exec: %s %s", ctx->filename, args);
    return 0;
    }
  2. 使用内核内置功能:

    1
    2
    # 使用ftrace直接获取参数
    echo 'sched:sched_process_exec cmd=$cmd args=$args' > trace
  3. 如果必须编写内核模块:

    • 使用probe_user_read()/strncpy_from_user_nofault()
    • 严格限制缓冲区大小
    • 处理所有可能的错误情况
    • 避免在关键路径中打印完整参数

总之,在 Linux 5.10 中:

  • 没有 copy_from_user_nofault() 函数
  • 使用 probe_user_read()strncpy_from_user_nofault() 替代
  • eBPF 是更安全、更推荐的方法

使用strncpy_from_user_nofault获取用户态进程cmdline

在 Linux 内核中,使用 strncpy_from_user_nofault() 获取用户态进程的 cmdline(即启动参数)是一种安全且高效的方法,尤其适用于 trace_sched_process_exec 这样的原子上下文。以下是完整的实现方案:


1. 核心实现代码(Linux 5.10+)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <linux/trace_events.h>
#include <linux/binfmts.h>
#include <linux/uaccess.h>
#include <linux/string.h>

#define MAX_ARG_LEN 256 // 单个参数最大长度
#define MAX_ARG_COUNT 32 // 最大参数数量
#define MAX_TOTAL_LEN 512 // 总参数缓冲区大小

static void tp_sched_process_exec(void *ignore,
struct task_struct *task,
pid_t old_pid,
struct linux_binprm *bprm)
{
unsigned long argv_user[MAX_ARG_COUNT];
char arg_buf[MAX_ARG_LEN];
char cmdline[MAX_TOTAL_LEN] = {0};
int i, offset = 0;
int argc = min(bprm->argc, MAX_ARG_COUNT);

// 1. 复制用户空间的参数指针数组(内核地址 -> 内核地址)
if (copy_from_kernel_nofault(argv_user, bprm->p, sizeof(unsigned long) * argc)) {
trace_printk("Failed to copy argv pointers\n");
return;
}

// 2. 逐个获取参数
for (i = 0; i < argc && offset < MAX_TOTAL_LEN - 1; i++) {
unsigned long arg_addr = argv_user[i];
long len;

// 安全复制用户空间字符串(无睡眠)
len = strncpy_from_user_nofault(arg_buf, (const char __user *)arg_addr, MAX_ARG_LEN);
if (len <= 0) continue; // 跳过无效参数

// 确保字符串终止
arg_buf[min(len, MAX_ARG_LEN - 1)] = '\0';

// 拼接参数(用空格分隔)
if (i > 0 && offset < MAX_TOTAL_LEN - 1) {
cmdline[offset++] = ' ';
}

// 复制到总缓冲区
strscpy(cmdline + offset, arg_buf, MAX_TOTAL_LEN - offset);
offset += strlen(arg_buf);
}

// 3. 输出结果(实际使用中建议用tracepoint或perf)
trace_printk("exec: pid=%d cmd=%s args=%s\n",
task->pid, bprm->filename, cmdline);
}

2. 关键点解析

(1) 安全访问用户空间

  • strncpy_from_user_nofault()
    内核提供的无睡眠函数,直接从用户空间复制字符串到内核,不会触发缺页中断
    参数:

    1
    long strncpy_from_user_nofault(char *dst, const void __user *src, long count);
    • 返回值:成功时返回字符串长度(不含终止符),错误时返回负值。
  • copy_from_kernel_nofault()
    用于安全复制内核空间数据(如 bprm->p 中的指针数组)。

(2) 边界保护

  • 限制参数数量和长度
    通过 MAX_ARG_COUNTMAX_ARG_LEN 防止恶意程序传递超长参数导致缓冲区溢出。

  • 使用 strscpy() 替代 strcpy
    确保目标缓冲区始终以 \0 终止,避免溢出。

(3) 原子上下文约束

  • 禁止睡眠操作
    所有函数(strncpy_from_user_nofaultcopy_from_kernel_nofault)均保证在原子上下文中安全执行。

3. 替代方案:eBPF(推荐用于生产环境)

若内核支持 eBPF,以下方案更安全高效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
SEC("tracepoint/sched/sched_process_exec")
int trace_exec(struct trace_event_raw_sched_process_exec *ctx)
{
struct linux_binprm *bprm = (struct linux_binprm *)ctx->bprm;
unsigned long argv[MAX_ARG_COUNT];
char cmdline[MAX_TOTAL_LEN] = {0};
int i, offset = 0;

// 1. 复制参数指针数组
bpf_probe_read_kernel(&argv, sizeof(argv), bprm->p);

// 2. 处理每个参数
for (i = 0; i < min(bprm->argc, MAX_ARG_COUNT); i++) {
char arg[MAX_ARG_LEN];
long len;

// 读取单个参数
len = bpf_probe_read_user_str(arg, sizeof(arg), (void *)argv[i]);
if (len <= 0) continue;

// 拼接参数
if (i > 0) {
bpf_snprintf(cmdline + offset, MAX_TOTAL_LEN - offset, " ");
offset++;
}
bpf_snprintf(cmdline + offset, MAX_TOTAL_LEN - offset, "%s", arg);
offset += len - 1; // 减去末尾的null
}

// 3. 输出到perf缓冲区
bpf_trace_printk("exec: %s %s", ctx->filename, cmdline);
return 0;
}

4. 注意事项

  1. 内核版本差异

    • strncpy_from_user_nofault 在 Linux 5.10+ 中可用,旧版本需用 probe_user_read()
    • linux_binprm 结构体可能随版本变化,需验证成员偏移量。
  2. 性能优化

    • 生产环境中避免打印完整 cmdline,可只记录关键信息(如哈希值)。
    • 对高频事件(如容器启动),建议通过 eBPF 的 perf_submit 异步处理数据。
  3. 安全风险

    • 用户空间参数可能包含恶意构造的超长字符串,需严格限制长度。
    • 敏感信息(如密码)可能出现在参数中,记录时需脱敏。

5. 验证方法

加载模块后,通过 dmesgtrace_pipe 查看输出:

1
2
3
4
5
# 查看内核日志
dmesg -wH

# 或直接读取trace输出(eBPF方案)
cat /sys/kernel/debug/tracing/trace_pipe

输出示例:

1
exec: pid=1234 cmd=/usr/bin/bash args=-c echo hello

以上方案在保证安全性的同时,完整获取了进程的启动参数,适用于调试、监控或安全审计场景。