Linux内核角度分析tcpdump原理(二)

2023-05-16

上篇文章介绍了在内核角度tcpdump的抓包原理(1),主要流程如下:

  • 应用层通过libpcap库:调用系统调用创建socket,sock_fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));tcpdump在socket创建过程中创建packet_type(struct packet_type),并挂载到全局的ptype_all链表上。(同时在packet_type设置回调函数packet_rcv。

  • 网络收包/发包时,会在各自的处理函数(收包时:__netif_receive_skb_core,发包时:dev_queue_xmit_nit)中遍历ptype_all链表,并同时执行其回调函数,这里tcpdump的注册的回调函数就是packet_rcv。

  • packet_rcv函数中会将用户设置的过滤条件,通过BPF进行过滤,并将过滤的数据包添加到接收队列中。

  • 应用层调用recvfrom 。PF_PACKET 协议簇模块调用packet_recvmsg 将接收队列中的数据copy应用层,到此将数据包捕获到。

本文围绕的重点是:BPF的过滤原理,如下源码所示:run_filter(skb, sk, snaplen),本次文章将对BPF的过滤原理进行一些分析。

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
        struct packet_type *pt, struct net_device *orig_dev)
{
  ......
         
 res = run_filter(skb, sk, snaplen);   //将用户指定的过滤条件使用BPF进行过滤
    ......
 __skb_queue_tail(&sk->sk_receive_queue, skb);//将skb放到当前的接收队列中
    ......

}

tcpdump依附标准的的BPF机器,tcpdump的过滤规则会被转化成一段bpf指令并加载到内核中的bpf虚拟机器上执行,显然,由用户来写过滤代码太过复杂,因此 libpcap 允许用户书写高层的、容易理解的过滤字符串,然后将其编译为BPF代码,tcpdump自动完成,不为用户所见。

一、BPF汇编指令集

关于BPF的介绍以及学习路线可以参考该链接的文章(https://linux.cn/article-9507-1.html),该文章给出了一些阅读清单。

BPF指令集

是一个伪机器码,与能够在物理机上直接执行的机器码不同,BPF指令集是可以在BPF虚拟机上执行的指令集。bpf在内核中实际就是一个虚拟机,有自己定义的虚拟机寄存器组。在最早的cBPF汇编框架中的三种寄存器:

Element          Description

  A                32 bit wide accumulator//所以加载指令的目的地址和所有指令运算结果的存储地址
  X                32 bit wide X register//二元指令计算A中参数的辅助寄存器(例如移位的位数,除法的除数)
  M[]              16 x 32 bit wide misc registers aka "scratch memory
                   store", addressable from 0 to 15// 0-15共16个32位寄存器,可以自由使用

在cBPF每条汇编指令如下这种格式:

//  include\uapi\linux\filter.h
struct sock_filter {    /* Filter block */
    __u16   code;   /* 真正的bpf汇编指令 */
    __u8    jt; /* 结果为true时跳转指令 */
    __u8    jf; /* 结果为false时跳转 */
    __u32   k;      /* 指令的参数 */
};

我们最常见的用法莫过于从数据包中取某个字的数据来做判断。按照bpf的规定,我们可以使用偏移来指定数据包的任何位置,而很多协议很常用并且固定,例如端口和ip地址等,bpf就为我们提供了一些预定义的变量,只要使用这个变量就可以直接取值到对应的数据包位置。例如:

  len                                   skb->len
  proto                                 skb->protocol
  type                                  skb->pkt_type
  poff                                  Payload start offset
  ifidx                                 skb->dev->ifindex
  nla                                   Netlink attribute of type X with offset A
  nlan                                  Nested Netlink attribute of type X with offset A
  mark                                  skb->mark
  queue                                 skb->queue_mapping
  hatype                                skb->dev->type
  rxhash                                skb->hash
  cpu                                   raw_smp_processor_id()
  vlan_tci                              skb_vlan_tag_get(skb)
  vlan_avail                            skb_vlan_tag_present(skb)
  vlan_tpid                             skb->vlan_proto
  rand                                  prandom_u32()

cBPF在一些平台还在使用,这个代码就和用户空间使用的那种汇编是一样的,但是在X86架构,现在在内核态已经都切换到使用eBPF作为中间语言了。由于用户可以提交cBPF的代码,所以首先是将用户提交来的结构体数组进行编译成eBPF代码(提交的是eBPF就不用了)。然后再将eBPF代码转变为可直接执行的二进制。eBPF汇编框架下的bpf语句如下:

//  include\uapi\linux\bpf.h
struct bpf_insn {
    __u8    code;       /* 存放真正的指令码 */
    __u8    dst_reg:4;  /* 存放指令用到的寄存器号(R0~R10) */
    __u8    src_reg:4;  /* 同上,存放指令用到的寄存器号(R0~R10) */
    __s16   off;        /* signed offset 取决于指令的类型*/
    __s32   imm;        /* 存放立即值 */
};

tcpdump -d

tcpdump支持使用-d参数来显示过滤规则转换后的bpf汇编指令。在抓包时我们并不关心如何具体的编写struct sock_filter内的东西,因为tcpdump已经内置了这样的功能。如想要对所接受的数据包过滤,只想抓取TCP协议、端口为8080数据包,那么在tcpdump当中的命令就是tcpdump ip and tcp port 8080 。如果你想让tcpdump帮你编译这样的过滤器,则用tcpdump  -d 'ip and tcp port 8080',如下案例(参考《Linux内核观测技术BPF》)如下图所示,显示“tcpdump抓取tcp端口8080数据包”的bpf汇编指令:

Image

(000) ldh      [12]
(001) jeq      #0x800           jt 2 jf 12
(002) ldb      [23]
(003) jeq      #0x6             jt 4 jf 12
(004) ldh      [20]
(005) jset     #0x1fff          jt 12 jf 6
(006) ldxb     4*([14]&0xf)
(007) ldh      [x + 14]
(008) jeq      #0x1f90          jt 11 jf 9
(009) ldh      [x + 16]
(010) jeq      #0x1f90          jt 11 jf 12
(011) ret      #262144
(012) ret      #0

为了进一步分析这条案例的流程,先把数据包的帧格式和ip数据报的格式放下面便于分析:

以太网帧格式:

Image

IP数据报格式:

Image

001-003条bpf汇编指令进行解释:

(000) ldh      [12]:

ldh指令表示累加器在偏移量12处进行加载一个半字(16位),从以太网帧的格式中可以看到偏移量为12字节处为以太网类型字段。

(001) jeq      #0x800           jt 2 jf 12:

jeq指令bioassay如果相等则跳转,也就是检查上一条指令返回的以太网类型的值是否为ox800(ipv4的标识),如果为true(jt),就跳转到指令2,否则跳转到指令12。

(002) ldb      [23]:

ldb指令在偏移量23字节处进行加载(计算一下:以太网帧的头部是14个字节,那么第23个字节,也就是IP头部的第9个字节),根据IP数据报的格式可以看到第9个字节是“协议”字段。

(003) jeq      #0x6             jt 4 jf 12

jeq指令根据第9个字节的值进行再一次的判断和跳转,如果第9个字节“协议字段”为0x6(TCP),如果是TCP则跳转到下一条指令004,否则跳转到012,数据包进行丢弃。

上面的规则对应代码结构体在Linux内核中的表示其实就是struct sock_filter,在libpcap库中对应的结构体为struct bpf_insn

struct bpf_insn {
 u_short code;
 u_char  jt;
 u_char  jf;
 bpf_u_int32 k;
};

tcpdump -dd

上述使用tcpdump -d看到了过滤规则转换后的bpf汇编指令,上面几个指令:ld开头的表示加载某地址数据,jeq是比较,jt就是jump when true,jf就是jump when false,后面表示行号;tcpdump支持使用-dd参数将匹配信息包的代码以c语言程序段的格式给出:

Image

像c当中的数组的定义,这个就是过滤tcp8080数据包的struct sock_filter的数组代码。

二、tcpdump设置BPF过滤器

在libpcap 设置过滤规则用到了两个接口,pcap_compile()和 pcap_setfilter ()

pcap_compile 函数的主要工作就是创建一个bpf的结构体,后面pcap_setfilter 会把生产的规则设置到内核,让规则生效。

struct pcap

在进行分析pcap_compile()和 pcap_setfilter ()前,先介绍一个结构体:struct pcap,介绍后面的过程便于查阅该结构体的成员变量,该结构体在libpcap源码的pcap-int.h中定义,该结构体抓包过程的一个句柄。

struct pcap 

{
    int fd; /* 文件描述字,实际就是 socket */
    
        /* 在 socket 上,可以使用 select() 和 poll() 等 I/O 复用类型函数 */
    int selectable_fd;

    int snapshot; /* 用户期望的捕获数据包最大长度 */
    int linktype; /* 设备类型 */
    int tzoff;        /* 时区位置,实际上没有被使用 */
    int offset;    /* 边界对齐偏移量 */

    int break_loop; /* 强制从读数据包循环中跳出的标志 */

    struct pcap_sf sf; /* 数据包保存到文件的相关配置数据结构 */
    struct pcap_md md; /* 具体描述如下 */
    
    int bufsize; /* 读缓冲区的长度 */
    u_char buffer; /* 读缓冲区指针 */
    u_char *bp;
    int cc;
    u_char *pkt;

    /* 相关抽象操作的函数指针,最终指向特定操作系统的处理函数 */
    int    (*read_op)(pcap_t *, int cnt, pcap_handler, u_char *);
    int    (*setfilter_op)(pcap_t *, struct bpf_program *);
    int    (*set_datalink_op)(pcap_t *, int);
    int    (*getnonblock_op)(pcap_t *, char *);
    int    (*setnonblock_op)(pcap_t *, int, char *);
    int    (*stats_op)(pcap_t *, struct pcap_stat *);
    void (*close_op)(pcap_t *);

    /*如果 BPF 过滤代码不能在内核中执行,则将其保存并在用户空间执行 */
    struct bpf_program fcode;

    /* 函数调用出错信息缓冲区 */
    char errbuf[PCAP_ERRBUF_SIZE + 1];
    
    /* 当前设备支持的、可更改的数据链路类型的个数 */
    int dlt_count;
    /* 可更改的数据链路类型号链表,在 linux 下没有使用 */
    int *dlt_list;

    /* 数据包自定义头部,对数据包捕获时间、捕获长度、真实长度进行描述 [pcap.h] */
    struct pcap_pkthdr pcap_header;    
};

/* 包含了捕获句柄的接口、状态、过滤信息  [pcap-int.h] */
struct pcap_md {
/* 捕获状态结构  [pcap.h] */
struct pcap_stat stat;  

    int use_bpf; /* 如果为1,则代表使用内核过滤*/
    u_long    TotPkts;
    u_long    TotAccepted; /* 被接收数据包数目 */
    u_long    TotDrops;    /* 被丢弃数据包数目 */
    long    TotMissed;    /* 在过滤进行时被接口丢弃的数据包数目 */
    long    OrigMissed; /*在过滤进行前被接口丢弃的数据包数目*/
#ifdef linux
    int    sock_packet; /* 如果为 1,则代表使用 2.0 内核的 SOCK_PACKET 模式 */
    int    timeout;    /* pcap_open_live() 函数超时返回时间*/
    int    clear_promisc; /* 关闭时设置接口为非混杂模式 */
    int    cooked;        /* 使用 SOCK_DGRAM 类型 */
    int    lo_ifindex;    /* 回路设备索引号 */
    char *device;    /* 接口设备名称 */
    
/* 以混杂模式打开 SOCK_PACKET 类型 socket 的 pcap_t 链表*/
struct pcap *next;    
#endif
};

pcap_compile

//函数用于将用户制定的过滤策略编译成BPF代码,然后存入bpf_program结构中
/*
p:pcap_open_live()返回的pcap_t类型的指针
fp:存放编译后的bpf
buf:过滤规则
optimize:是否需要优化过滤表达式
mask:指定本地网络的网络掩码,不需要时可以设置为0
*/
pcap_compile(pcap_t *p,struct bpf_program *fp,char *filterstr,int opt,bpf_u_int32 netmask);

pcap_setfilter

//将过滤的规则注入内核
int pcap_setfilter (pcap_t *p, struct bpf_program *fp)

函数原型:

int pcap_setfilter(pcap_t *p, struct bpf_program *fp)
{
 return (p->setfilter_op(p, fp)); //在Linux中将调用pcap_setfilter_linux
}

调用pcap_setfilter_linux:

static int pcap_setfilter_linux(pcap_t *handle, struct bpf_program *filter)
{
 return pcap_setfilter_linux_common(handle, filter, 0);
}

进一步调用pcap_setfilter_linux_common:

static int pcap_setfilter_linux_common(pcap_t *handle, struct bpf_program *filter,
    int is_mmapped)
{  
    ......
    struct sock_fprog fcode;
 ......
    if (install_bpf_program(handle, filter) < 0)//把BPF代码拷贝到pcap_t 数据结构的fcode上
    ......
       /*Linux内核设置过滤器时使用的数据结构是sock_fprog,而不是BPF的结构bpf_program,因此应做结构之间的转换*/
  switch (fix_program(handle, &fcode, is_mmapped)) {
       //严重错误直接退出
  case -1:
  default:
   return -1;
       //通过检查,但不能工作在内核中
  case 0:
   
   can_filter_in_kernel = 0;
   break;
        //BPF可以在内核中工作
  case 1:

   can_filter_in_kernel = 1;
   break;
  }
 }
     //通过检查后,如果可以则在内核中安装过滤器
 if (can_filter_in_kernel) {
  if ((err = set_kernel_filter(handle, &fcode)) == 0)
  {

   handlep->filter_in_userland = 0;
  }
    ......

 return 0;
}

上面涉及到的Linux内核中的struct sock_fprog和libpcap库中的struct bpf_program如下所示:

struct sock_fprog {   /* Required for SO_ATTACH_FILTER. */
 unsigned short     len; /* Number of filter blocks */
 struct sock_filter __user *filter;
};
struct bpf_program {
 u_int bf_len;
 struct bpf_insn *bf_insns;//该结构体上面介绍过,相当于Linux内核中的struct sock_filte
};

install_bpf_program(handle, filter)的拷贝过程:

//该函数主要将libpcap bpf规则结构体,转换成符合liunx内核的bpf规则,同时校验规则是否符合要求格式。
int install_bpf_program(pcap_t *p, struct bpf_program *fp)
{
 ......
 pcap_freecode(&p->fcode);//释放可能存在的BPF代码

 prog_size = sizeof(*fp->bf_insns) * fp->bf_len;//计算过滤代码的长度,分配内存空间
 p->fcode.bf_len = fp->bf_len;
 p->fcode.bf_insns = (struct bpf_insn *)malloc(prog_size);
 if (p->fcode.bf_insns == NULL) {
  snprintf(p->errbuf, sizeof(p->errbuf),
    "malloc: %s", pcap_strerror(errno));
  return (-1);
 }
    //把过滤代码保存在捕获句柄中
 memcpy(p->fcode.bf_insns, fp->bf_insns, prog_size);//p->fcode就是struct bpf_program
 return (0);
}

pcap_setfilter_linux_common最终会在set_kernel_filter中调用setsockopt系统调用(执行到这才真正进入内核,开始在Linux内核上安装和设置BPF过滤器),通过SO_ATTACH_FILTER 下发给内核底层,从而让规则生效,设置过滤器。

static int  set_kernel_filter(pcap_t *handle, struct sock_fprog *fcode)
{
 ......
 ret = setsockopt(handle->fd, SOL_SOCKET, SO_ATTACH_FILTER,
    fcode, sizeof(*fcode)); 
    ......
}

在liunx上,只需要简单的创建的filter代码,通过SO_ATTTACH_FILTER选项发送到内核,并且filter代码能通过内核的检查,这样你就可以立即过滤socket上面的数据了。

三、setsockopt()

Linux 在安装和卸载过滤器时都使用了函数 setsockopt(),其中标志SOL_SOCKET 代表了对 socket 进行设置,而 SO_ATTACH_FILTER 和 SO_DETACH_FILTER 则分别对应了安装和卸载。

  • 在套接字socket 附加filter规则 :

setsockopt(sockfd, SOL_SOCKET, SO_ATTACH_FILTER, &val, sizeof(val));

  • ·把filter从socket上移除 :

setsockopt(sockfd, SOL_SOCKET, SO_DETACH_FILTER, &val, sizeof(val));

Linux内核在sock_setsockopt函数中进行设置:

//:  net\core\sock.c
int sock_setsockopt(struct socket *sock, int level, int optname,
      char __user *optval, unsigned int optlen)
{
 ......
 case SO_ATTACH_FILTER:
  ret = -EINVAL;
  if (optlen == sizeof(struct sock_fprog)) {
   struct sock_fprog fprog;

   ret = -EFAULT;
            //把过滤条件结构体从用户空间拷贝到内核空间
   if (copy_from_user(&fprog, optval, sizeof(fprog)))
    break;
            //在socket上安装过滤器
   ret = sk_attach_filter(&fprog, sk);
  }
  break;
    ......

 case SO_DETACH_FILTER:
      //解除过滤器
  ret = sk_detach_filter(sk);
  break;

 ......
}

上面出现的 sk_attach_filter() 定义在 net/core/filter.c,它把结构sock_fprog 转换为结构 sk_filter, 最后把此结构设置为 socket 的过滤器:sk->filter = fp。

回到文章开始(抓包的引入):

static int packet_rcv(struct sk_buff *skb, struct net_device *dev,
        struct packet_type *pt, struct net_device *orig_dev)
{
  ......
         
 res = run_filter(skb, sk, snaplen);   //将用户指定的过滤条件使用BPF进行过滤
    ......
 __skb_queue_tail(&sk->sk_receive_queue, skb);//将skb放到当前的接收队列中
    ......

}

run_filter:

static unsigned int run_filter(struct sk_buff *skb,const struct sock *sk,unsigned int res)
{
 struct sk_filter *filter;

 rcu_read_lock();
 filter = rcu_dereference(sk->sk_filter);//获取之前设置的过滤器
 if (filter != NULL)
  res = bpf_prog_run_clear_cb(filter->prog, skb);//进行数据包过滤
 rcu_read_unlock();

 return res;
}

综上,结合上一篇文章,和本文就将Linux内核角度将tcpdump的工作原理分析完毕。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux内核角度分析tcpdump原理(二) 的相关文章

  • flex布局知识点Beta

    先看图 xff0c 实现下方列表中的效果 xff0c 名字在左 xff0c 数量在右 xff0c 相对条目居中 html代码 span class hljs tag lt span class hljs title view span sp
  • js如何一次循环删除数组中的多个元素

    思路 xff1a 数组遍历删除一个元素很容易 xff0c 通过splice方法删除对应索引的元素即可 xff0c 但是遍历删除多个元素就复杂了很多 xff0c 首先不能按索引从小到大的顺序删除 xff0c 这样可能会导致索引对应的元素发生变
  • git删除远程分支

    两步删除远程仓库 xff1a git branch r d origin dev 删除远程分支 git push origin dev然后提交到远程 注意 xff1a dev分支前的冒号 不能少
  • nodejs基础篇(一)

    我从2017年开始接触nodejs xff0c 到现在用了一年多了 xff0c 觉得我有必要写点东西出来 xff0c 记录自己的积累的知识体系 xff0c 以便更好的掌握nodejs nodejs专题不是按照严格的学习历程书写 xff0c
  • 滚动到顶部的实现方法

    span class token comment scrollTop animation span span class token keyword export span span class token keyword function
  • 浅谈ES6的Promise对象

    相信凡是写过javascript的童鞋也一定都写过回调方法 xff08 callback xff09 xff0c 简单说回调方法就是将一个方法func2作为参数传入另一个方法func1中 xff0c 当func1执行到某一步或者满足某种条件
  • node版本管理器——nvm

    nvm是管理node版本的一个工具 xff0c 具体介绍不再赘述 xff0c 请到GitHub xff08 https github com creationix nvm xff09 查看 安装 Linux Mac curl o https
  • centos下修改mysql默认端口

    mysql5 6安装 xff1a br wget http repo mysql com mysql community release el7 5 noarch rpm br rpm ivh mysql community release
  • nginx开启gzip压缩

    nginx安装 xff1a yum install y nginx 配置文件默认在 etc nginx nginx conf 打开nginx conf添加 gzip span class hljs function start span c
  • wordpress安装后问题汇总

    问题一 xff1a wordpress写文章界面点击 添加媒体 和 可视化 文本 无反应 解决方法 xff1a 在wp config php中追加 define 34 CONCATENATE SCRIPTS 34 false 问题二 xff
  • ucos ii是怎么实现多任务运行的?很通俗易懂的描述

    问题 xff1a ucos上建立一个任务 xff0c 格式如上图 xff0c 它是一个死循环 xff0c 但如果我建立了五个任务 xff0c 并且五个任务里面没有延时 xff0c 就只是像无操作系统那样写法 xff0c 用死循环让它们一直跑
  • 解决vim中文乱码

    执行 xff1a cd xff5e vim vimrc 将如下文本复制保存退出即可 set fileencodings 61 utf 8 ucs bom gb18030 gbk gb2312 cp936 set termencoding 6
  • nginx开启ssl证书

    修改listen为443 如果你又开启防火墙 xff0c 还需将443端口放开 在server中添加 ssl on ssl session timeout 5m ssl certificate cert 214050429370638 pe
  • 一键安装全局npm模块

    GitHub xff1a https github com flitrue npm install global 简介 当我们使用nvm管理node xff0c 切换node版本时 xff0c 安装在全局的npm包无法共用之前node版本中
  • vscode设置命令行启动

    方法一 xff1a 配置 启动 VS Code打开命令面板 shift 43 ctrl 43 P mac shift 43 cmmand 43 P xff0c 输入 shell command xff0c 找到 Install code c
  • Vue和React的生命周期

    最新的Vue和React生命周期 Vue 生命周期 vue生命周期主要有8个 beforeCreatecreatedbeforeMountmountedbeforeUpdateupdatedbeforeDestroydestroyed Re
  • js实现千位分隔符运算

    方法一 xff1a span class token keyword function span span class token function format span span class token punctuation span
  • 调整浏览器滚动条样式

    我们知道浏览器自带滚动条很丑 xff0c 有时影响整个页面到美观 xff0c 尤其在页面内嵌一个滚动列表 xff0c 显得奇丑无比 xff0c 下面我们根据如下代码调节滚动条样式 span class token punctuation s
  • 视觉SLAM理论入门——(7)视觉里程计之特征点法—对极几何

    提取ORB特征后 xff0c 需要根据点估计相机的运动 根据相机的原理 xff0c 可以分为下面几种情况 xff1a 1 当采用单目相机 xff0c 只知道2D像素坐标 xff0c 需要根据两组2D点估计运动 xff0c 这时用对极几何求解
  • 驱动程序之_1_字符设备_13_USB设备_3_鼠标驱动

    驱动程序之 1 字符设备 13 USB设备 3 鼠标驱动 从上一篇文章知道 xff0c 当我们接入一个USB设备 xff0c USB总线驱动会为我们构建一个device并注册 xff0c 编写驱动程序时只需要构造driver并注册到总线即可

随机推荐

  • 【转帖】驱动程序与应用程序之间的通信

    驱动程序必须与应用程序进行通信 xff0c 才能最终达到应用程序控制设备的目的 xff0c 不然驱动有QIU用 要通信就涉及到3个方面 1 应用程序与驱动程序通信 2 驱动程序与应用程序通信 3 数据传输 下面分别讨论 1 应用程序与驱动程
  • STM32最小系统硬件组成详解

    STM32最小系统硬件组成详解 0组成 xff1a 电源 复位 时钟 调试接口 启动 1 电源 xff1a 一般3 3V LDO供电 加多个0 01uf去耦电容 2 复位 xff1a 有三种复位方式 xff1a 上电复位 手动复位 程序自动
  • 从特效入手,深入了解CSS(一):动态加载特效

    不建议跳跃阅读 xff01 这篇文章将从头开始介绍如何实现一个特效 中间偶尔会穿插一些css3或平时接触不多的css属性 首先看一看这一期的特效 xff1a HTML部分 span class token tag span class to
  • Nuxt.js数据预取

    本文采用的技术框架有 xff1a 后台 xff1a Express 43 MongoDb 前台 xff1a Vue2 js 43 Nuxt js 64 2 9 2 在Nuxt中发送请求有两种方案 xff1a 前后台分离的方案 xff08 数
  • 【matplotlib】matplotlib使用详解 使用python绘制漂亮的论文数据图

    预备知识 Matplotlib 是 Python 中最受欢迎的数据可视化软件包之一 xff0c 支持跨平台运行 xff0c 它是 Python 常用的 2D 绘图库 xff0c 同时它也提供了一部分 3D 绘图接口 Matplotlib 通
  • apt-get指令的autoclean,clean,autoremove的区别

    下面总结一下有关apt get的常用但容易混淆的指令 br strong apt get autoclean strong 如果你的硬盘空间不大的话 xff0c 可以定期运行这个程序 xff0c 将已经删除了的软件包的 deb安装文件从硬盘
  • 2021美团笔试题(第十套)个人解答

    1 淘汰分数 span class token comment 暴力解法 span span class token keyword import span span class token namespace java span clas
  • DeepSOCIAL:基于YOLOv4的人群距离监测!集检测、跟踪以及逆透视映射一体的系统!...

    点击下方 AI算法与图像处理 xff0c 一起进步 xff01 重磅干货 xff0c 第一时间送达 论文 xff1a https doi org 10 3390 app10217514 代码 xff1a https github com D
  • HAL 0.5.10 Specification

    HAL 0 5 10 Specification David Zeuthen lt a href david 64 fubar dk a gt Version 0 5 10 Table of Contents 1 Introduction
  • LINUX USB 系统(1) 收藏

    LINUX USB 系统 1 收藏 1 简述 xff1a USB 出自豪门 xff0c 一问世便有 IBM Microsoft compaq 等前呼后拥 xff0c 不红实在是没有道理 xff0c 以致于连三岁小毛孩都知道买游戏手柄要买 U
  • 高通brew 方案开机揭秘

    摘要 xff1a 本文试图通过代码来深入剖析 Qualcomm 手机开机的整个过程 xff0c 即从按下开机键一直到出现待机界面 xff0c Qualcomm 的手机软件在整个流程中究竟完成了哪些工作 本文的主要目标是理清手机的初始化流程
  • 编程之美读书笔记_3.3_计算字符串的相似度

    3 3 计算字符串的相似度 和计算两字符串的最长公共子序列相似 设Ai 为字符串A a1a2a3 am 的前i 个字符 xff08 即为a1 a2 a3 ai xff09 设Bj 为字符串B b1b2b3 bn 的前j 个字符 xff08
  • 有铅喷锡和无铅喷锡的选择

    有铅喷锡和无铅喷锡 xff08 SAC xff09 在生产中工艺要求是一个非常重要的因素 xff0c 他直接决定着一个PCB板的质量和定位 xff0c 比如喷锡 镀金 沉金 xff0c 相对来说沉金就是面对高端的板子 xff0c 沉金由于质
  • 硬盘的那些事(主分区、扩展分区、逻辑分区、活动分区、系统分区、启动分区、引导扇区、MBR等

    硬盘的那些事 xff08 主分区 扩展分区 逻辑分区 活动分区 系统分区 启动分区 引导扇区 MBR等 主分区 xff0c 扩展分区 xff0c 逻辑分区 xff0c 活动分区 xff0c 系统分区 xff0c 启动分区 主引导扇区 xff
  • Vcc(电源)和GND(地)之间接电容的作用

    1 在直流电源 xff08 Vcc xff09 和地之间并接电容的电容可称为滤波电容 xff0e 滤波电容滤除电源的杂波和交流成分 xff0c 压平滑脉动直流电 xff0c 储存电能 xff0e 取值一般100 xff0d 4700uF x
  • APM/Pixhawk地面站航迹规划指令单

    本文来源于http ardupilot org copter docs mission command list html condition distance 由于对APM Pixhawk的爱好 xff0c 翻译成中文供参考 xff0c
  • APM/Pixhawk路径规划飞行(自动起飞/降落/航路点飞行)

    APM Pixhawk路径规划飞行 xff08 自动起飞 降落 航路点飞行 xff09 本节主要介绍各类飞行器一般的航路点设置 xff0c 已经实现了自动起飞降落和按计划轨迹飞行 设置家的位置 对于直升机和多旋翼家的位置一般是飞控解锁的位置
  • E2上GBA模拟器移植的困难。。。

    终于决心动手写GBA的模拟器了 修改了半晌 xff0c 终于吧Oop的模拟器源码编译通过 xff0c 但是到了机器上却跑不起来 xff0c 那个源码用了一个非常奇怪的方式来处理系统响应 xff1a 主动调用系统去处理 xff0c 没开线程
  • 蓝牙物理链路类型:SCO和ACL链路

    蓝牙物理链路ACL Asynchronous Connectionless 另外的一种链路是SCO Synchronous Connection Oriented 主要用来传输对时间要求很高的数据通信 蓝牙基带技术支持两种连接类型 xff1
  • Linux内核角度分析tcpdump原理(二)

    上篇文章介绍了在内核角度tcpdump的抓包原理 1 xff0c 主要流程如下 xff1a 应用层通过libpcap库 xff1a 调用系统调用创建socket sock fd 61 socket PF PACKET SOCK RAW ht