kprobes技术介绍+原理,ebpf中的kprobe+运行情况+用户层代码+内核层代码+预编译代码(详细解释+语法介绍),修改.bpf.c入口处的函数,系统调用和内核函数的区别

目录

kprobes

引入

介绍

三种探测手段

kprobe

示例代码

jprobe

Kretprobe

查看支持的函数

原理

ebpf的kprobe 

引入

运行情况 

代码解释

.bpf.c

源码

语法 / 函数接口

char LICENSE[] SEC("license") = "Dual BSD/GPL"

SEC

SEC("kprobe/do_unlinkat")

系统调用和内核函数的区别

动态kprobe的两种插入形式

do_unlinkat

filename结构体

​编辑

BPF_KPROBE                    

bpf_get_current_pid_tgid()

BPF_CORE_READ ()

bpf_printk

.bpf.i

编译语句

源码 -- 入口处函数

语法

 __attribute__((section("kprobe/do_unlinkat"), used))

__attribute__((always_inline)) 

typeof(do_unlinkat(0)) 

pt_regs  

return ____do_unlinkat(ctx, (void *)((ctx)->di), (void *)((ctx)->si))

 ____fmt

bpf_trace_printk

修改.bpf.c中入口处的函数定义

预编码代码

源码 -- 出口处函数

语法 

return ____do_unlinkat_exit(ctx, (void *)((ctx)->ax))

.c

源码

语法 

libbpf_print_fn()

volatile

sig_atomic_t 

kprobe_bpf 

libbpf_set_print(libbpf_print_fn);


kprobes

引入

开发人员在内核或者模块的调试过程中,往往会需要要知道其中的一些函数有无被调用、何时被调用、执行是否正确以及函数的入参和返回值是什么等等

  • 比较简单的做法是在内核代码对应的函数中添加日志打印信息,但这种方式往往需要重新编译内核或模块,重新启动设备之类的
  • 操作较为复杂甚至可能会破坏原有的代码执行过程

  • 但利用kprobes技术,用户可以定义自己的回调函数,然后在内核或者模块中几乎所有的函数中动态地插入探测点
  • 当内核执行流程执行到指定的探测函数时,会调用该回调函数,用户即可收集所需的信息,同时内核最后还会回到原本的正常执行流程

介绍

Kprobes是Linux内核中的一个功能,它允许开发人员在内核空间中进行动态的代码跟踪和调试

  • 可以在内核代码的任何位置插入调试代码,以便监视和调试内核中发生的事件或特定函数的执行情况
  • 一般是在函数的入口/出口处插入钩子(也可以叫探测点),利用注册的回调函数,知道内核函数是否被调用,被调用上下文,入参以及返回值

三种探测手段

kprobes 技术包括的3种探测手段分别是 kprobe、jprobe 和 kretprobe

kprobe

kprobe 是最基本的探测方式,是实现后两种的基础

  • 它可以在任意的位置放置探测点(就连函数内部的某条指令处也可以)
  • 它提供了探测点的调用前、调用后和内存访问出错3种回调方式,分别是 pre_handlerpost_handler 和 fault_handler

其中

  • pre_handler 函数将在被探测指令被执行前回调
  • post_handler 会在被探测指令执行完毕后回调(注意不是被探测函数)
  • fault_handler 会在内存访问出错时被调用
示例代码
/*
 * NOTE: This example is works on x86 and powerpc.
 * Here's a sample kernel module showing the use of kprobes to dump a
 * stack trace and selected registers when do_fork() is called.
 *
 * For more information on theory of operation of kprobes, see
 * Documentation/kprobes.txt
 *
 * You will see the trace data in /var/log/messages and on the console
 * whenever do_fork() is invoked to create a new process.
 */

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/kprobes.h>

/* For each probe you need to allocate a kprobe structure */
static struct kprobe kp = {
	.symbol_name	= "do_fork",
};

/* kprobe pre_handler: called just before the probed instruction is executed */
static int handler_pre(struct kprobe *p, struct pt_regs *regs)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, ip = %lx,"
			" flags = 0x%lx\n",
		p->addr, regs->ip, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "pre_handler: p->addr = 0x%p, nip = 0x%lx,"
			" msr = 0x%lx\n",
		p->addr, regs->nip, regs->msr);
#endif

	/* A dump_stack() here will give a stack backtrace */
	return 0;
}

/* kprobe post_handler: called after the probed instruction is executed */
static void handler_post(struct kprobe *p, struct pt_regs *regs,
				unsigned long flags)
{
#ifdef CONFIG_X86
	printk(KERN_INFO "post_handler: p->addr = 0x%p, flags = 0x%lx\n",
		p->addr, regs->flags);
#endif
#ifdef CONFIG_PPC
	printk(KERN_INFO "post_handler: p->addr = 0x%p, msr = 0x%lx\n",
		p->addr, regs->msr);
#endif
}

/*
 * fault_handler: this is called if an exception is generated for any
 * instruction within the pre- or post-handler, or when Kprobes
 * single-steps the probed instruction.
 */
static int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr)
{
	printk(KERN_INFO "fault_handler: p->addr = 0x%p, trap #%dn",
		p->addr, trapnr);
	/* Return 0 because we don't handle the fault. */
	return 0;
}

static int __init kprobe_init(void)
{
	int ret;
	kp.pre_handler = handler_pre;
	kp.post_handler = handler_post;
	kp.fault_handler = handler_fault;

	ret = register_kprobe(&kp);
	if (ret < 0) {
		printk(KERN_INFO "register_kprobe failed, returned %d\n", ret);
		return ret;
	}
	printk(KERN_INFO "Planted kprobe at %p\n", kp.addr);
	return 0;
}

static void __exit kprobe_exit(void)
{
	unregister_kprobe(&kp);
	printk(KERN_INFO "kprobe at %p unregistered\n", kp.addr);
}

module_init(kprobe_init)
module_exit(kprobe_exit)
MODULE_LICENSE("GPL");
jprobe

基于 kprobe 实现,它用于获取被探测函数的入参值 

Kretprobe

是Kprobe的补充,它允许在函数返回时插入调试代码

 

查看支持的函数

 不是所有的函数都支持kprobe机制,可以通过cat查看当前系统支持的函数

cat /sys/kernel/debug/tracing/available_filter_functions

 

原理

内核函数会像用户态暴露自己的内存地址

  • 查看暴露的内核函数地址:cat /proc/kallsyms
  • Kprobes通过访问内核函数的内存地址,即可向可探测的内核函数的某个位置增加断点(一组回调函数)
  • 当实际执行到放置探测点的内核函数的断点时,跳转指令会跳转到Kprobes句柄注册的探测函数(内存地址)上
  • 而后执行结束,再利用句柄跳转回原来的上下文
  • 像触发了中断机制一样
  • 也可以理解成就是在函数开始/返回时调用了函数

 

ebpf的kprobe 

引入

在 eBPF 中,Kprobe 也是一种类型的探测点,用于在指定的内核函数入口或出口插入探测点,并执行用户定义的 eBPF 程序

  • eBPF Kprobe 提供了比传统 Kprobes 更丰富的功能和灵活性,可以编写更复杂的程序来处理探测点触发时的事件
  • 使用ebpf,可以更方便的使用kprobe机制

 

以下是libbpf-bootstrap库中的kprobe示例代码: 

运行情况 

  •  就是在执行文件删除时,会调用底层的do_unlinkat内核函数
  • 如果我们将探测点设置在该函数的入/出口处,就会在内核日志中看到自定义的打印信息

代码解释

.bpf.c

源码

// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/* Copyright (c) 2021 Sartura */
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>

char LICENSE[] SEC("license") = "Dual BSD/GPL";

SEC("kprobe/do_unlinkat")
int BPF_KPROBE(do_unlinkat, int dfd, struct filename *name)
{
	pid_t pid;
	const char *filename;

	pid = bpf_get_current_pid_tgid() >> 32;
	filename = BPF_CORE_READ(name, name);
	bpf_printk("KPROBE ENTRY pid = %d, filename = %s\n", pid, filename);
	return 0;
}

SEC("kretprobe/do_unlinkat")
int BPF_KRETPROBE(do_unlinkat_exit, long ret)
{
	pid_t pid;

	pid = bpf_get_current_pid_tgid() >> 32;
	bpf_printk("KPROBE EXIT: pid = %d, ret = %ld\n", pid, ret);
	return 0;
}

这就是在某个函数入口和出口处分别注册的两个函数的定义

语法 / 函数接口

char LICENSE[] SEC("license") = "Dual BSD/GPL"

将许可证信息与特定的eBPF程序相关联,确保对程序的合适授权和管理

  • 定义了一个名为LICENSE的全局字符数组
  • "license"是一个自定义的安全上下文名称,用于将字符数组与特定的上下文相关联(这里其实不太懂原理,可能就是把它存起来让内核识别了??)
  • "Dual BSD/GPL" -- 指示eBPF程序的许可证类型
  • 总之,记住在写代码时不要漏掉这个许可证即可
SEC

SEC(Security)用于定义eBPF程序的安全上下文

  • 相当于是一种通行证,告诉内核ebpf程序接下来的行为,以确定被允许进入哪些区域,执行哪些操作
  • 指定程序是否可以访问特定的系统资源(如内存、网络、文件系统)、是否可以执行特定的系统调用,以及程序在执行时是否需要特权等
  • 他其实是一种宏定义,在下面查看预编译代码中会讲到
SEC("kprobe/do_unlinkat")

是eBPF程序中的一种语法 -- ("kprobe形式/要挂接的内核函数名")

  • 它指定了一个eBPF程序应该连接到哪个内核函数
  • 注意是内核函数,而不是系统调用
系统调用和内核函数的区别
  • 内核函数是由操作系统内核提供的函数,用于执行特定的内核操作,这些函数可以直接在内核空间中被调用,一般是提供给其他内核模块或内核中的其他部分使用
  • 系统调用是用户空间程序通过软中断或陷阱方式请求操作系统内核提供的服务的接口,它们允许用户空间程序访问内核空间的功能
动态kprobe的两种插入形式
  • "kprobe"表示将该eBPF程序连接到内核中的"do_unlinkat"函数的入口处
  • "kretprobe"表示将该eBPF程序连接到该函数的出口处(也就是即将返回时会调用该函数)
do_unlinkat

是一个内核函数,用于实现unlinkat系统调用的功能

int unlinkat(int dirfd, const char *pathname, int flags);
  • dirfd 是一个文件描述符,它引用了一个目录
  • pathname 是要删除的文件或者目录的路径
  • flags 可以是 0,也可以是 AT_REMOVEDIR
  • 如果是 AT_REMOVEDIR,那么 pathname 就会被当作目录处理
filename结构体

是内核中存放文件名的结构体

  • name字段就是存放文件名的字符串
BPF_KPROBE                    

一个宏定义

  • 使用BPF的Kprobe的声明语句,格式是 -- (探针函数名,该函数的参数列表)
  • 这样在预编译后,编译器会自动为我们生成这样一个函数,作为探测函数
bpf_get_current_pid_tgid()

可以在eBPF程序中获取当前执行程序(也就是调用了do_unlinkat内核函数的进程)的PID和TGID,以便进行进程相关的操作或统计

  • 比如:可以筛选特定进程,计算进程的资源使用情况等
BPF_CORE_READ ()

读取内核中的数据

  • 是一个宏定义
  • 原型类似于:
  • #define BPF_CORE_READ(struct_name, field_name) \
        ({                                          \
            typeof(struct_name.field_name) _val;   \
            bpf_probe_read(&_val, sizeof(_val), ((void *)bpf_core_field(struct_name, field_name))); \
            _val;                                   \
        })
  • 意思是读取内核中某个核心数据结构的字段,将其作为返回值返回

  • filename = BPF_CORE_READ(name, name)就是指拿到文件名,而这个文件名被定义在name结构体(就是上面的filename结构体,它指针名字叫name)里的name字段

bpf_printk

用于在内核日志(trace_pipe(/sys/kernel/debug/tracing/trace_pipe))中打印消息

  • 内核日志也可以叫做内核环形缓冲区
  • 相当于就是c语言中printf -- 将数据拷贝到标准输出流中,它通常被重定向到终端设备,所以可以将它刷新到显示器上
  • 可以输出实时的内核跟踪事件数据流
  • 最多只能有三个参数

.bpf.i

编译语句

bpf,ebpf,libbpf,libbpf_bootstarp概念介绍(如何安装libbpf_bootstarp库),以及四者关系,ebpf程序执行流程(代码分层,具体如何编译,后续操作,关系总结)-CSDN博客

  • 相当于就是在最后一句里加-E选项,目标文件名改为.bpf.i

源码 -- 入口处函数

 __attribute__((section("kprobe/do_unlinkat"), used))

//函数声明
int do_unlinkat(struct pt_regs *ctx); 

static inline __attribute__((always_inline)) typeof(do_unlinkat(0)) ____do_unlinkat(struct pt_regs *ctx, int dfd, void *name);

//函数体
typeof(do_unlinkat(0)) do_unlinkat(struct pt_regs *ctx) 
{
    return ____do_unlinkat(ctx, (void *)((ctx)->di), (void *)((ctx)->si));
} 

static inline typeof(do_unlinkat(0))____do_unlinkat(struct pt_regs *ctx, int dfd, struct filename *name)
{
    pid_t pid;
    const char *filename;

    pid = bpf_get_current_pid_tgid() >> 32;
    filename = ({ 
        typeof((name)->name) __r; 
        ({ 
            bpf_probe_read_kernel((void *)(&__r), sizeof(*(&__r)), (const void *)__builtin_preserve_access_index(&((typeof(((name))))(((name))))->name)); 
        }); 
        __r; 
    });

    ({ 
        static const char ____fmt[] = "KPROBE ENTRY pid = %d, filename = %s\n"; 
        bpf_trace_printk(____fmt, sizeof(____fmt), pid, filename); 
    });
    return 0;
}

相当于就是把自己定义的参数和函数体放在____do_unlinkat,自动生成一个do_unlinkat进行回调

  • 但是注意,参数列表有变化:之前写的do_unlinkat字段变成了一个pt_regs结构体

语法

 __attribute__((section("kprobe/do_unlinkat"), used))

这个就是SEC的宏展开

  • 将变量或函数放置在指定的 ELF 节(section)
  • used -- 确保它在链接过程中被使用
__attribute__((always_inline)) 

指示编译器始终内联此函数  

typeof(do_unlinkat(0)) 
  • 返回do_unlinkat函数的返回值类型
  • 相当于是动态获取函数的返回值类型,而不是写死的
pt_regs  
  • 自动生成的do_unlinkat函数的参数类型(一个结构体指针)
  • 通常用于保存处理系统调用或中断时的寄存器状态
  • 还可以看看这个定义:
  • 可以把这六个字段看作是系统调用时的参数顺序
  • (说实话不太懂,照猫画虎着写吧)
  • 或者也可以使用宏提取参数 -- PT_REGS_PARM1(pt_regs结构体指针)
return ____do_unlinkat(ctx, (void *)((ctx)->di), (void *)((ctx)->si))
  • 总之,我们可以发现,我们传入的参数,其实都是在编译时自动帮我们获取并传参了
  • 核心就是 pt_regs  结构体
 ____fmt

占位符,用于表示消息的格式化字符串

  • 定义消息的格式,以便在bpf_trace_printk中使用
bpf_trace_printk

eBPF 程序中的辅助函数

  • 用于将消息打印到内核日志中
  • 最多只能传三个参数

修改.bpf.c中入口处的函数定义

其实从预编译代码可以看出来,我们自定义的函数最重要的其实就是函数体,而参数都可以从pt_regs 结构体中获得

所以,我们可以考虑修改修改这个函数

 SEC("kprobe/do_unlinkat")

 int test(struct pt_regs *ctx)
 {
     pid_t pid;
     const char *filename;
     struct filename *name = (struct filename *)(ctx->si);

     pid = bpf_get_current_pid_tgid() >> 32;
     filename = BPF_CORE_READ(name, name);
     bpf_printk("KPROBE ENTRY-test  pid = %d, filename = %s\n", pid,
            filename); 
     return 0;
 }
  • 函数名可以随意起
  • 参数其实只要有个pt_regs 结构体指针就行,而且不用加do_unlinkat(因为已经指定过要连接的是哪个内核函数)
  • 需要的文件名信息可以手动提取出来

预编码代码


  __attribute__((section("kprobe/do_unlinkat"), used))
 int test(struct pt_regs *ctx)
 {
      pid_t pid;
      const char *filename = (const char *)(ctx->si);

      pid = bpf_get_current_pid_tgid() >> 32;
      ({ static const char ____fmt[] = "KPROBE ENTRYpid = %d, filename = %s\n"; bpf_trace_printk(____fmt, sizeof(____fmt), pid, filename); });

      return 0;
 }

可以看出来,我们写的代码其实就和处理后代码很相近了

源码 -- 出口处函数

__attribute__((section("kretprobe/do_unlinkat"), used))

 typeof(do_unlinkat_exit(0)) do_unlinkat_exit(struct pt_regs *ctx)
 {
 	return ____do_unlinkat_exit(ctx, (void *)((ctx)->ax));
 }

 static inline attribute__((always_inline)) typeof(do_unlinkat_exit(0))
 	____do_unlinkat_exit(struct pt_regs *ctx, long ret)
 {
 	pid_t pid;

 	pid = bpf_get_current_pid_tgid() >> 32;
 	({
 		static const char ____fmt[] = "KPROBE EXIT: pid = %d, ret = %ld\n";
 		bpf_trace_printk(____fmt, sizeof(____fmt), pid, ret);
 	});
 	return 0;
 }

语法 

基本和入口处那的差不多

return ____do_unlinkat_exit(ctx, (void *)((ctx)->ax))

总之就是,先记住这些寄存器的用法

.c

源码

// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause)
/* Copyright (c) 2021 Sartura
 * Based on minimal.c by Facebook */

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/resource.h>
#include <bpf/libbpf.h>
#include "kprobe.skel.h"

static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
{
	return vfprintf(stderr, format, args);
}

static volatile sig_atomic_t stop;

static void sig_int(int signo)
{
	stop = 1;
}

int main(int argc, char **argv)
{
	struct kprobe_bpf *skel;
	int err;

	/* Set up libbpf errors and debug info callback */
	libbpf_set_print(libbpf_print_fn);

	/* Open load and verify BPF application */
	skel = kprobe_bpf__open_and_load();
	if (!skel) {
		fprintf(stderr, "Failed to open BPF skeleton\n");
		return 1;
	}

	/* Attach tracepoint handler */
	err = kprobe_bpf__attach(skel);
	if (err) {
		fprintf(stderr, "Failed to attach BPF skeleton\n");
		goto cleanup;
	}

	if (signal(SIGINT, sig_int) == SIG_ERR) { //防止自定义信号处理方法失败
		fprintf(stderr, "can't set signal handler: %s\n", strerror(errno));
		goto cleanup;
	}

	printf("Successfully started! Please run `sudo cat /sys/kernel/debug/tracing/trace_pipe` "
	       "to see output of the BPF programs.\n");

	while (!stop) {  //如果没有收到SIGINT信号,就会打印.以表示自己仍在运行中(ebpf程序依然还挂载在内核中)
		fprintf(stderr, ".");
		sleep(1);
	}

cleanup:
	kprobe_bpf__destroy(skel);
	return -err;
}

语法 

libbpf_print_fn()

volatile

表示该变量可能会被异步修改,告诉编译器不要进行优化 

sig_atomic_t 

保证了对该变量的读取和写入是原子操作的,即不会被信号中断 

kprobe_bpf 

表示一个 kprobe(内核探测点)的 eBPF 程序

  • 在使用 eBPF 进行 kprobe 事件跟踪时,通常会创建一个包含相应逻辑的结构体
  • 相当于是用于管理eBPF程序的一个结构体,可以把它和进程的tcb进行类比
  • 它是在.skel.中定义的,说明它会根据内核层代码自动生成,里面会存放用于管理和操作 bpf程序的字段
libbpf_set_print(libbpf_print_fn);

将自定义的打印函数传递给libbpf库,以便其在执行过程中使用

其中的level参数,是一个枚举类型:

  • 37
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 追踪Linux TCP/IP代码运行的过程,可以通过以下几个步骤实施: 首先,需要获得Linux内核的源代码,并找到TCP/IP协议栈的相关代码文件。这些文件通常位于“net”目录下,涉及到TCP/IP的部分大多在“net/ipv4”或“net/ipv6”目录下。 然后,我们可以选择使用调试器(如GDB)或者打印日志的方式来跟踪代码运行。如果选择使用调试器,可以在代码设置断点,观察程序在断点处的执行情况,查看变量的值和函数调用关系。如果使用打印日志的方式,可以在关键的代码段插入打印语句,输出相关变量的值和执行路径。 接着,我们需要了解TCP/IP协议栈的基本工作原理。具体来说,可以关注TCP的连接建立、数据传输和连接关闭等过程,以及IP协议的路由选择和分组转发等操作。 在追踪过程,可以选择从应用开始,如HTTP请求的发送和响应的接收。通过查看应用调用栈以及涉及的网络函数,可以逐步跟进到TCP/IP协议栈的代码。同时,还可以通过观察网络数据包的发送和接收过程来了解底数据在协议栈的处理过程。 在跟踪代码的过程,需要仔细观察代码的注释和文档,以及相关的技术文档和论文,以便理解代码的设计思路和实现细节。 最后,可以通过调试器或分析日志的结果来验证自己对代码运行过程的理解,并进行必要的修复和改进。 总之,追踪Linux TCP/IP代码运行是一个复杂而具有挑战性的任务,需要对Linux内核和网络协议有一定的了解和熟悉。同时,需要具备一定的编程和调试能力,以便在代码找到关键的位置,并通过调试工具或日志信息来进行代码分析。 ### 回答2: 要追踪Linux TCP/IP代码运行,可以按照以下步骤进行: 1. 首先,了解TCP/IP协议栈的基本原理和相关概念,包括网络、传输和应用的功能和协议。 2. 掌握Linux内核开发的基础知识,包括Linux内核的编译、调试和运行环境的搭建。 3. 下载并编译Linux内核代码,可以从官方网站或开源代码库获取最新的Linux内核源码。 4. 在源代码找到相关的TCP/IP代码,可以通过在源代码搜索关键字或使用开发工具进行定位。 5. 使用调试工具来分析和追踪代码运行Linux内核提供了一些调试工具如Ftrace、Kprobes和SystemTap等,可以用于监视和分析内核函数调用运行过程。 6. 添加自定义的调试输出或断点来获取更详细代码执行信息。通过在代码插入printk语句或断点来观察特定变量的值或代码执行路径。 7. 使用网络抓包工具来捕获和分析网络数据包,可以验证TCP/IP代码的正确性和性能。 8. 进行测试和验证,使用各种测试工具和场景来验证TCP/IP代码的正确性和性能。 9. 探索并阅读现有的文档、论文和开发者社区的讨论,可以从获取更多关于Linux TCP/IP代码运行和调试技巧。 总之,追踪Linux TCP/IP代码运行需要深入理解 TCP/IP协议栈的原理,掌握Linux内核开发的基础知识,使用调试工具和网络抓包工具来分析和验证代码运行。同时,与开发者社区保持联系,探索和学习他人的经验和技巧。 ### 回答3: 要追踪Linux TCP/IP代码运行,首先需要一个源代码的PDF文件。我们可以从官方Linux内核网站上下载相关的源码文件,然后将其导出为PDF格式。 追踪代码运行的第一步是了解整体的调用流程。打开PDF文件,可以从文件的目录找到TCP/IP模块的相关代码。这些文件包括传输代码(如tcp.c)和网络代码(如ip.c),它们负责TCP/IP协议的实现。 在PDF文件,我们可以使用搜索功能找到特定的函数名或关键字。比如,如果希望追踪TCP的建立过程,可以搜索"tcp_accept"函数。找到这个函数后,可以查看它的调用关系,看它在哪些地方被调用,以及它又调用了哪些函数。 在追踪过程,要注意一些关键的数据结构,例如TCP控制块(TCB)和IP包头。这些结构包含了关键的信息,如IP地址、端口号和序列号等。可以通过搜索这些结构的定义,并在代码追踪它们的使用。 另外,还可以使用调试工具来加速追踪的过程。在Linux系统,可以使用GDB(GNU调试器)来逐步执行代码,并观察变量的值。这可以帮助我们更直观地理解代码的执行流程。 追踪Linux TCP/IP代码运行需要耐心和坚持,需要仔细阅读和理解大量的代码。通过结合源代码的PDF文件和调试工具的使用,我们可以更好地理解代码的逻辑和执行过程。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值