关于Linux内核eBPF的探索_第1页
关于Linux内核eBPF的探索_第2页
关于Linux内核eBPF的探索_第3页
关于Linux内核eBPF的探索_第4页
关于Linux内核eBPF的探索_第5页
已阅读5页,还剩53页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

Linux内核在2022年主要发布了5.16-5.19以及6.0和6.1这几个版本,每个版本都为eBPF引入了大量的新特性。本文将对这些新特性进行一点简要的介绍,更详细的资料请参考对应的链接信息。总体而言,eBPF在内核中依然是最活跃的模块之一,它的功能特性也还在高速发展中。某种意义上说,eBPF正朝着一个完备的内核态可编程接口快速进化。eBPF进阶:内核新特性进展一览BPFkfuncsBloomFilterMap:5.16CompileOnce–RunEverywhere:Linux5.17bpf_loop()辅助函数:5.17BPF_LINK_TYPE_KPROBE_MULTI:5.18动态指针和类型指针:5.19USDT:5.19bpfpanic:6.1BPF内存分配器、链表:6.1userringbuffer6.1一、eBPF概述1.1eBPF是什么eBPF是一个基于寄存器的虚拟机,使用自定义的64位RISC指令集,能够在Linux内核内运行即时本地编译的“BPF程序”,并能访问内核功能和内存的一个子集。这是一个完整的虚拟机实现,不要与基于内核的虚拟机(KVM)相混淆,后者是一个模块,目的是使Linux能够作为其他虚拟机的管理程序。eBPF也是主线内核的一部分,所以它不像其他框架那样需要任何第三方模块(LTTng或SystemTap),而且几乎所有的Linux发行版都默认启用。熟悉DTrace的读者可能会发现DTrace/BPFtrace对比非常有用。在内核内运行一个完整的虚拟机主要是考虑便利和安全。虽然eBPF程序所做的操作都可以通过正常的内核模块来处理,但直接的内核编程是一件非常危险的事情-这可能会导致系统锁定、内存损坏和进程崩溃,从而导致安全漏洞和其他意外的效果,特别是在生产设备上(eBPF经常被用来检查生产中的系统),所以通过一个安全的虚拟机运行本地JIT编译的快速内核代码对于安全监控和沙盒、网络过滤、程序跟踪、性能分析和调试都是非常有价值的。部分简单的样例可以在这篇优秀的eBPF参考中找到。基于设计,eBPF虚拟机和其程序有意地设计为不是图灵完备的:即不允许有循环(正在进行的工作是支持有界循环【译者注:已经支持有界循环,#pragmaunroll指令】),所以每个eBPF程序都需要保证完成而不会被挂起、所有的内存访问都是有界和类型检查的(包括寄存器,一个MOV指令可以改变一个寄存器的类型)、不能包含空解引用、一个程序必须最多拥有BPF_MAXINSNS指令(默认4096)、“主"函数需要一个参数(context)等等。当eBPF程序被加载到内核中,其指令被验证模块解析为有向环状图,上述的限制使得正确性可以得到简单而快速的验证。主要区别如下:允许使用C语言编写代码片段,并通过LLVM编译成eBPF字节码;cBPF只实现了SOCKET_FILTER,而eBPF还有KPROBE、PERF等。BPF使用socket实现了用户态与内核交互,eBPF则定义了一个专用于eBPF的新的系统调用,用于装载BPF代码段、创建和读取BPFmap,更加通用。BPFmap机制,用于在内核中以key-value的方式临时存储BPF代码产生的数据。对于eBPF可以简单的理解成kernel实现了一个虚拟机机制,将类C代码编译成字节码(后文有详细解释),挂在到内核的钩子上,当钩子被触发时,kernel在虚拟机的"沙盒"中运行字节码,这样既能方便的实现很多功能,也能通过沙箱保证内核的安全性。1.2eBPF的演进最初的[BerkeleyPacketFilter(BPF)PDF]是为捕捉和过滤符合特定规则的网络包而设计的,过滤器为运行在基于寄存器的虚拟机上的程序。在内核中运行用户指定的程序被证明是一种有用的设计,但最初BPF设计中的一些特性却并没有得到很好的支持。例如,虚拟机的指令集架构(ISA)相对落后,现在处理器已经使用64位的寄存器,并为多核系统引入了新的指令,如原子指令XADD。BPF提供的一小部分RISC指令已经无法在现有的处理器上使用。因此AlexeiStarovoitov在eBPF的设计中介绍了如何利用现代硬件,使eBPF虚拟机更接近当代处理器,eBPF指令更接近硬件的ISA,便于提升性能。其中最大的变动之一是使用了64位的寄存器,并将寄存器的数量从2提升到了10个。由于现代架构使用的寄存器远远大于10个,这样就可以像本机硬件一样将参数通过eBPF虚拟机寄存器传递给对应的函数。另外,新增的BPF_CALL指令使得调用内核函数更加便利。将eBPF映射到本机指令有助于实时编译,提升性能。3.15内核中新增的eBPF补丁使得x86-64上运行的eBPF相比老的BPF(cBPF)在网络过滤上的性能提升了4倍,大部分情况下会保持1.5倍的性能提升。很多架构(x86-64,SPARC,PowerPC,ARM,arm64,MIPS,ands390)已经支持即时(JIT)编译。1.3ebpf环境搭建编译运行源码samples/bpf中的代码下载内核源码并解压/bin/sh:scripts/mod/modpost:Nosuchfileordirectory遇到这种错误,需要makescriptsmakeM=samples/bpf需要.config文件,需要保证这些项存在遇到错误libcrypt1.so.1notfound,执行如下代码(/debian-bugs-dist@/msg1818037.html)$cd/tmp

$apt-ydownloadlibcrypt1

$dpkg-deb-xlibcrypt1_1%3a4.4.25-2_amd64.deb.

$cp-avlib/x86_64-linux-gnu/*/lib/x86_64-linux-gnu/

$apt-y--fix-brokeninstall5.编译成功,可以执行samples/bpf中的可执行文件。编译运行自己开发的代码1.下载linuxsourcecode,编译内核并升级gitclone/torvalds/linux.git

cdlinux/

gitcheckout-bv5.0v5.0配置文件cp-a/boot/config-4.14.81.bm.15-amd64./.config

echo'

CONFIG_BPF=y

CONFIG_BPF_SYSCALL=y

CONFIG_BPF_JIT=y

CONFIG_HAVE_EBPF_JIT=y

CONFIG_BPF_EVENTS=y

CONFIG_FTRACE_SYSCALLS=y

CONFIG_FUNCTION_TRACER=y

CONFIG_HAVE_DYNAMIC_FTRACE=y

CONFIG_DYNAMIC_FTRACE=y

CONFIG_HAVE_KPROBES=y

CONFIG_KPROBES=y

CONFIG_KPROBE_EVENTS=y

CONFIG_ARCH_SUPPORTS_UPROBES=y

CONFIG_UPROBES=y

CONFIG_UPROBE_EVENTS=y

CONFIG_DEBUG_FS=y

CONFIG_DEBUG_INFO_BTF=y

'>>./.config需要添加sid源安装dwarvesaptinstalldwarves

makeoldconfig

aptinstalllibssl-dev

make

makemodules_install

makeinstall

reboot此时:uname-a

Linuxn231-238-0615.0.0#1SMPMonDec1305:38:52UTC2021x86_64GNU/Linux编译bpfhelloworld切换到/bpftools/linux-observability-with-bpf的helloworld目录sed-i's;/kernel-src;/root/linux;'Makefile

make有报错:参考:/blog/2021/build-kernel-ebpf-sample/解决cp

/root/linux/include/uapi/linux/bpf.h

/usr/include/linux/bpf.h执行./monitor-exec,有报错./monitor-exec:

error

while

loading

shared

libraries:

libbpf.so:

cannot

open

shared

object

file:

No

such

file

or

directory解决方法cd/root/linux/tools/lib/bpf/

make

makeinstall在/etc/ld.so.conf中添加/usr/local/lib64这一行,运行sudoldconfig重新生成动态库配置信息。~/linux/tools/lib/bpf#ldconfig-v2>/dev/null|greplibbpf

libbpf.so.0->libbpf.so.0.5.0

libbpf.so->libbpf.so最终执行情况:可能需要安装apt-getinstallgcc-multilibg++-multilib/sirfz/tesserocr/issues/130安装bpftrace(1)debian添加sid源/iovisor/bcc/blob/master/INSTALL.md#debiansourcedeb/debiansidmaincontribnon-free

deb-src/debiansidmaincontribnon-free(2)aptinstallbpftrace/iovisor/bpftrace/blob/master/INSTALL.md1.4使用eBPF可以做什么?一个eBPF程序会附加到指定的内核代码路径中,当执行该代码路径时,会执行对应的eBPF程序。鉴于它的起源,eBPF特别适合编写网络程序,将该网络程序附加到网络socket,进行流量过滤,流量分类以及执行网络分类器的动作。eBPF程序甚至可以修改一个已建链的网络socket的配置。XDP工程会在网络栈的底层运行eBPF程序,高性能地进行处理接收到的报文。从下图可以看到eBPF支持的功能:BPF对网络的处理可以分为tc/BPF和XDP/BPF,它们的主要区别如下(参考该文档):XDP的钩子要早于tc,因此性能更高:tc钩子使用sk_buff结构体作为参数,而XDP使用xdp_md结构体作为参数,sk_buff中的数据要远多于xdp_md,但也会对性能造成一定影响,且报文需要上送到tc钩子才会触发处理程序。由于XDP钩子位于网络栈之前,因此XDP使用的xdp_buff(即xdp_md)无法访问sk_buff元数据。structxdp_buff{

/*Linux5.8*/

void*data;

void*data_end;

void*data_meta;

void*data_hard_start;

structxdp_rxq_info*rxq;

structxdp_txq_info*txq;

u32frame_sz;/*framesizetodeducedata_hard_end/reservedtailroom*/

};

structxdp_rxq_info{

structnet_device*dev;

u32queue_index;

u32reg_state;

structxdp_mem_infomem;

}____cacheline_aligned;/*perfcritical,avoidfalse-sharing*/

structxdp_txq_info{

structnet_device*dev;};data指向page中的数据包的其实位置,data_end指向数据包的结尾。由于XDP允许headroom(见下文),data_hard_start指向page中headroom的起始位置,即,当对报文进行封装时,data会通过bpf_xdp_adjust_head()向data_hard_start移动。相同的BPF辅助函数也可以用以解封装,此时data会远离data_hard_start。data_meta一开始指向与data相同的位置,但bpf_xdp_adjust_meta()能够将其朝着data_hard_start移动,进而给用户元数据提供空间,这部分空间对内核网络栈是不可见的,但可以被tcBPF程序读取(tc需要将它从XDP转移到skb)。反之,可以通过相同的BPF程序将data_meta远离data_hard_start来移除或减少用户元数据大小。data_meta还可以单纯地用于在尾调用间传递状态,与tcBPF程序访问的skb->cb[]控制块类似。

对于structxdp_buff中的报文指针,有如下关系:data_hard_start<=data_meta<=data<data_end。rxq字段指向在ring启动期间填充的额外的与每个接受队列相关的元数据。BPF程序可以检索queue_index,以及网络设备上的其他数据(如ifindex等)。tc能够更好地管理报文:tc的BPF输入上下文是一个sk_buff,不同于XDP使用的xdp_buff,二者各有利弊。当内核的网络栈在XDP层之后接收到一个报文时,会分配一个buffer,解析并保存报文的元数据,这些元数据即sk_buff。该结构体会暴露给BPF的输入上下文,这样tcingress层的tcBPF程序就能够使用网络栈从报文解析到的元数据。使用sk_buff,tc可以更直接地使用这些元数据,因此附加到tcBPF钩子的BPF程序可以读取或写入skb的mark,pkt_type,protocol,priority,queue_mapping,napi_id,cb[]array,hash,tc_classid或tc_index,vlanmetadata等,而XDP能够传输用户的元数据以及其他信息。tcBPF使用的struct__sk_buff定义在linux/bpf.h头文件中。xdp_buff的弊端在于,其无法使用sk_buff中的数据,XDP只能使用原始的报文数据,并传输用户元数据。XDP的能够更快地修改报文:sk_buff包含很多协议相关的信息(如GSO阶段的信息),因此其很难通过简单地修改报文数据达到切换协议的目的,原因是网络栈对报文的处理主要基于报文的元数据,而非每次访问数据包内容的开销。因此,BPF辅助函数需要正确处理内部sk_buff的转换。而xdp_buff则不会有这种问题,因为XDP的处理时间早于内核分配sk_buff的时间,因此可以简单地实现对任何报文的修改(但管理起来要更加困难)。

tc/ebpf和xdp可以互补:如果用户需要修改报文,同时对数据进行比较复杂的管理,那么,可以通过运行两种类型的程序来弥补每种程序类型的局限性。XDP程序位于ingress,可以修改完整的报文,并将用户元数据从XDPBPF传递给tcBPF,然后tc可以使用XDP的元数据和sk_buff字段管理报文。

tc/eBPF可以作用于ingress和egress,但XDP只能作用于ingress:与XDP相比,tcBPF程序可以在ingress和egress的网络数据路径上触发,而XDP只能作用于ingress。

tc/BPF不需要改变硬件驱动,而XDP通常会使用native驱动模式来获得更高的性能。但tcBPF程序的处理仍作用于早期的内核网络数据路径上(GRO处理之后,协议处理和传统的iptables防火墙的处理之前,如iptablesPREROUTING或nftablesingress钩子等)。而在egress上,tcBPF程序在将报文传递给驱动之前进行处理,即在传统的iptables防火墙(如iptablesPOSTROUTING)之后,但在内核的GSO引擎之前进行处理。一个特殊情况是,如果使用了offloaded的tcBPF程序(通常通过SmartNIC提供),此时Offloadedtc/eBPF接近于Offloaded

XDP的性能。从下图可以看到TC和XDP的工作位置,可以看到XDP对报文的处理要先于TC:内核执行的另一种过滤类型是限制进程可以使用的系统调用。通过seccompBPF实现。eBPF也可以用于通过将程序附加到tracepoints,

kprobes,和perfevents的方式定位内核问题,以及进行性能分析。因为eBPF可以访问内核数据结构,开发者可以在不编译内核的前提下编写并测试代码。对于工作繁忙的工程师,通过该方式可以方便地调试一个在线运行的系统。此外,还可以通过静态定义的追踪点调试用户空间的程序(即BCC调试用户程序,如Mysql)。使用eBPF有两大优势:快速,安全。为了更好地使用eBPF,需要了解它是如何工作的。1.5内核的eBPF校验器在内核中运行用户空间的代码可能会存在安全和稳定性风险。因此,在加载eBPF程序前需要进行大量校验。首先通过对程序控制流的深度优先搜索保证eBPF能够正常结束,不会因为任何循环导致内核锁定。严禁使用无法到达的指令;任何包含无法到达的指令的程序都会导致加载失败。第二个阶段涉及使用校验器模拟执行eBPF程序(每次执行一个指令)。在每次指令执行前后都需要校验虚拟机的状态,保证寄存器和栈的状态都是有效的。严禁越界(代码)跳跃,以及访问越界数据。校验器不会检查程序的每条路径,它能够知道程序的当前状态是否是已经检查过的程序的子集。由于前面的所有路径都必须是有效的(否则程序会加载失败),当前的路径也必须是有效的,因此允许验证器“修剪”当前分支并跳过其模拟阶段。校验器有一个"安全模式",禁止指针运算。当一个没有CAP_SYS_ADMIN特权的用户加载eBPF程序时会启用安全模式,确保不会将内核地址泄露给非特权用户,且不会将指针写入内存。如果没有启用安全模式,则仅允许在执行检查之后进行指针运算。例如,所有的指针访问时都会检查类型,对齐和边界冲突。无法读取包含未初始化内容的寄存器,尝试读取这类寄存器中的内容将导致加载失败。R0-R5的寄存器内容在函数调用期间被标记未不可读状态,可以通过存储一个特殊值来测试任何对未初始化寄存器的读取行为;对于读取堆栈上的变量的行为也进行了类似的检查,确保没有指令会写入只读的帧指针寄存器。最后,校验器会使用eBPF程序类型(见下)来限制可以从eBPF程序调用哪些内核函数,以及访问哪些数据结构。例如,一些程序类型可以直接访问网络报文。、1.6pf()系统调用使用bpf()系统调用和BPF_PROG_LOAD命令加载程序。该系统调用的原型为:int

bpf(int

cmd,

union

bpf_attr

*attr,

unsigned

int

size);bpf_attr允许数据在内核和用户空间传递,具体类型取决于cmd参数。cmd可以是如下内容:BPF_MAP_CREATE

Createamapandreturnafiledescriptorthatreferstothe

map.Theclose-on-execfiledescriptorflag(seefcntl(2))is

automaticallyenabledforthenewfiledescriptor.

BPF_MAP_LOOKUP_ELEM

Lookupanelementbykeyinaspecifiedmapandreturnits

value.

BPF_MAP_UPDATE_ELEM

Createorupdateanelement(key/valuepair)inaspecified

map.

BPF_MAP_DELETE_ELEM

Lookupanddeleteanelementbykeyinaspecifiedmap.

BPF_MAP_GET_NEXT_KEY

Lookupanelementbykeyinaspecifiedmapandreturnthe

keyofthenextelement.

BPF_PROG_LOAD

VerifyandloadaneBPFprogram,returninganewfiledescrip‐

torassociatedwiththeprogram.Theclose-on-execfile

descriptorflag(seefcntl(2))isautomaticallyenabledfor

thenewfiledescriptor.size参数给出了bpf_attr联合体对象的字节长度。BPF_PROG_LOAD加载的命令可以用于创建和修改eBPFmaps,maps是普通的key/value数据结构,用于在eBPF程序和内核空间或用户空间之间通信。其他命令允许将eBPF程序附加到一个控制组目录或socket文件描述符上,迭代所有的maps和程序,以及将eBPF对象固定到文件,这样在加载eBPF程序的进程结束后不会被销毁(后者由tc分类器/操作代码使用,因此可以将eBPF程序持久化,而不需要加载的进程保持活动状态)。完整的命令可以参考bpf()帮助文档。虽然可能存在很多不同的命令,但大体可以分为两类:与eBPF程序交互的命令,与eBPFmaps交互的命令,或同时与程序和maps交互的命令(统称为对象)。1.7eBPF程序类型使用BPF_PROG_LOAD加载的程序类型确定了四件事:附加的程序的位置,验证器允许调用的内核辅助函数,是否可以直接访问网络数据报文,以及传递给程序的第一个参数对象的类型。实际上,程序类型本质上定义了一个API。创建新的程序类型甚至纯粹是为了区分不同的可调用函数列表(例如,BPF_PROG_TYPE_CGROUP_SKB和BPF_PROG_TYPE_SOCKET_FILTER)。当前内核支持的eBPF程序类型为:BPF_PROG_TYPE_SOCKET_FILTER:anetworkpacketfilterBPF_PROG_TYPE_KPROBE:determinewhetherakprobeshouldfireornotBPF_PROG_TYPE_SCHED_CLS:anetworktraffic-controlclassifierBPF_PROG_TYPE_SCHED_ACT:anetworktraffic-controlactionBPF_PROG_TYPE_TRACEPOINT:determinewhetheratracepointshouldfireornotBPF_PROG_TYPE_XDP:anetworkpacketfilterrunfromthedevice-driverreceivepathBPF_PROG_TYPE_PERF_EVENT:determinewhetheraperfeventhandlershouldfireornotBPF_PROG_TYPE_CGROUP_SKB:anetworkpacketfilterforcontrolgroupsBPF_PROG_TYPE_CGROUP_SOCK:anetworkpacketfilterforcontrolgroupsthatisallowedtomodifysocketoptionsBPF_PROG_TYPE_LWT_*:anetworkpacketfilterforlightweighttunnelsBPF_PROG_TYPE_SOCK_OPS:aprogramforsettingsocketparametersBPF_PROG_TYPE_SK_SKB:anetworkpacketfilterforforwardingpacketsbetweensocketsBPF_PROG_CGROUP_DEVICE:determineifadeviceoperationshouldbepermittedornot随着新程序类型的增加,内核开发人员也会发现需要添加新的数据结构。1.8eBPF数据结构eBPF使用的主要的数据结构是eBPFmap,这是一个通用的数据结构,用于在内核或内核和用户空间传递数据。其名称"map"也意味着数据的存储和检索需要用到key。使用bpf()系统调用创建和管理map。当成功创建一个map后,会返回与该map关联的文件描述符。关闭相应的文件描述符的同时会销毁map。每个map定义了4个值:类型,元素最大数目,数值的字节大小,以及key的字节大小。eBPF提供了不同的map类型,不同类型的map提供了不同的特性。BPF_MAP_TYPE_HASH:ahashtableBPF_MAP_TYPE_ARRAY:anarraymap,optimizedforfastlookupspeeds,oftenusedforcountersBPF_MAP_TYPE_PROG_ARRAY:anarrayoffiledescriptorscorrespondingtoeBPFprograms;usedtoimplementjumptablesandsub-programstohandlespecificpacketprotocolsBPF_MAP_TYPE_PERCPU_ARRAY:aper-CPUarray,usedtoimplementhistogramsoflatencyBPF_MAP_TYPE_PERF_EVENT_ARRAY:storespointerstostructperf_event,usedtoreadandstoreperfeventcountersBPF_MAP_TYPE_CGROUP_ARRAY:storespointerstocontrolgroupsBPF_MAP_TYPE_PERCPU_HASH:aper-CPUhashtableBPF_MAP_TYPE_LRU_HASH:ahashtablethatonlyretainsthemostrecentlyuseditemsBPF_MAP_TYPE_LRU_PERCPU_HASH:aper-CPUhashtablethatonlyretainsthemostrecentlyuseditemsBPF_MAP_TYPE_LPM_TRIE:alongest-prefixmatchtrie,goodformatchingIPaddressestoarangeBPF_MAP_TYPE_STACK_TRACE:storesstacktracesBPF_MAP_TYPE_ARRAY_OF_MAPS:amap-in-mapdatastructureBPF_MAP_TYPE_HASH_OF_MAPS:amap-in-mapdatastructureBPF_MAP_TYPE_DEVICE_MAP:forstoringandlookingupnetworkdevicereferencesBPF_MAP_TYPE_SOCKET_MAP:storesandlooksupsocketsandallowssocketredirectionwithBPFhelperfunctions所有的map都可以通过eBPF或在用户空间的程序中使用bpf_map_lookup_elem()和bpf_map_update_elem()函数进行访问。某些map类型,如socketmap,会使用其他执行特殊任务的eBPF辅助函数。eBPF的更多细节可以参见官方帮助文档。注:

在Linux4.4之前,bpf()要求调用者具有CAP_SYS_ADMINcapability权限,从Linux4.4.开始,非特权用户可以使用BPF_PROG_TYPE_SOCKET_FILTER类型和相应的map创建受限的程序,然而这类程序无法将内核指针保存到map中,仅限于使用如下辅助函数:

*

get_random*

get_smp_processor_id*

tail_call*

ktime_get_ns

可以通过sysctl禁用非特权访问:

/proc/sys/kernel/unprivileged_bpf_disabled

eBPF对象(maps和程序)可以在不同的进程间共享。例如,在fork之后,子进程会继承引用eBPF对象的文件描述符。此外,引用eBPF对象的文件描述符可以通过UNIX域socket传输。引用eBPF对象的文件描述符可以通过dup(2)和类似的调用进行复制。当所有引用对象的文件描述符关闭后,才会释放eBPF对象。eBPF程序可以使用受限的C语言进行编写,并使用clang编译器编译为eBPF字节码。受限的C语言会禁用很多特性,如循环,全局变量,浮点数以及使用结构体作为函数参数。可以在内核源码的samples/bpf/*_kern.c文件中查看例子。

内核中的just-in-time(JIT)可以将eBPF字节码转换为机器码,提升性能。在Linux4.15之前,默认会禁用JIT,可以通过修改/proc/sys/net/core/bpf_jit_enable启用JIT。0禁用JIT1正常编译2dehub模式。从Linux4.15开始,内核可能会配置CONFIG_BPF_JIT_ALWAYS_ON选项,这种情况下,会启用JIT编译器,bpf_jit_enable会被设置为1。如下架构支持eBPF的JIT编译器:*x86-64(sinceLinux3.18;cBPFsinceLinux3.0);*ARM32(sinceLinux3.18;cBPFsinceLinux3.4);*SPARC32(sinceLinux3.18;cBPFsinceLinux3.5);*ARM-64(sinceLinux3.18);*s390(sinceLinux4.1;cBPFsinceLinux3.7);*PowerPC64(sinceLinux4.8;cBPFsinceLinux3.1);*SPARC64(sinceLinux4.12);*x86-32(sinceLinux4.18);*MIPS64(sinceLinux4.18;cBPFsinceLinux3.16);*riscv(sinceLinux5.1)1.9eBPF辅助函数可以参考官方帮助文档查看libbpf库提供的辅助函数。官方文档给出了现有的eBPF辅助函数。更多的实例可以参见内核源码的samples/bpf/和tools/testing/selftests/bpf/目录。在官方帮助文档中有如下补充:由于在编写帮助文档的同时,也同时在进行eBPF开发,因此新引入的eBPF程序或map类型可能没有及时添加到帮助文档中,可以在内核源码树中找到最准确的描述:

include/uapi/linux/bpf.h:主要的BPF头文件。包含完整的辅助函数列表,以及对辅助函数使用的标记,结构体和常量的描述

net/core/filter.c:包含大部分与网络有关的辅助函数,以及使用的程序类型列表

kernel/trace/bpf_trace.c:包含大部分与程序跟踪有关的辅助函数

kernel/bpf/verifier.c:包含特定辅助函数使用的用于校验eBPFmap有效性的函数

kernel/bpf/:该目录中的文件包含了其他辅助函数(如cgroups,sockmaps等)如何编写eBPF程序历史上,需要使用内核的bpf_asm汇编器将eBPF程序转换为BPF字节码。幸运的是,LLVMClang编译器支持将C语言编写的eBPF后端编译为字节码。bpf()系统调用和BPF_PROG_LOAD命令可以直接加载包含这些字节码的对象文件。可以使用C编写eBPF程序,并使用Clang的-march=bpf参数进行编译。在内核的samples/bpf/目录下有很多eBPF程序的例子。大多数文件名中都有一个_kern.c后缀。Clang编译出的目标文件(eBPF字节码)需要由一个本机运行的程序进行加载(通常为使用_user.c开头的文件)。为了简化eBPF程序的编写,内核提供了libbpf库,可以使用辅助函数来加载,创建和管理eBPF对象。例如,一个eBPF程序和使用libbpf的用户程序的大体流程为:在用户程序中读取eBPF字节流,并将其传递给bpf_load_program()。当在内核中运行eBPF程序时,将会调用bpf_map_lookup_elem()在一个map中查找元素,并保存一个新的值。用户程序会调用bpf_map_lookup_elem()读取由eBPF程序保存的内核数据。然而,大部分的实例代码都有一个主要的缺点:需要在内核源码树中编译自己的eBPF程序。幸运的是,BCC项目解决了这类问题。它包含了一个完整的工具链来编写并加载eBPF程序,而不需要链接到内核源码树。二、eBPF框架在开始说明之前先解释下eBPF上的名词,来帮忙更好的理解:eBPFbytecode:将C语言写的钩子代码,通过clang编译成二进制字节码,通过程序加载到内核中,钩子触发后在kernel"虚拟机"中运行。JIT:Just-in-timecompilation,将字节码编译成本地机器码来提升运行速度,和Java中的概念类似。Maps:钩子代码可以将一些统计类信息保存在键值对的map中,来与用户空间程序进行通信,传递数据。关于eBPF机制详细的讲解网上有很多,这里就不展开了,这里先上一张图,这里包括了使用或者编写ebpf涉及到的所有东西,下面会对这个图进行详细的讲解。foo_kern.c钩子实现代码,主要负责:声明使用的Map节点声明钩子挂载点及处理函数通过LLVM/clang编译成字节码编译命令:clang--target=bpfandroid平台有集成eBPF的编译,后文会提到foo_user.c用户空间处理函数,主要负责:将foo_kern.c编译成的字节码加载到kenel中读取Map中的信息并处理输出给用户kernel当收到eBPF的加载请求时,会先对字节码进行验证,并通过JIT编译为机器码,当钩子事件来临后,调用钩子函数,kernel会对加载的字节码进行验证,来保证系统的安全性,主要验证规则如下:a.检查是否声明了GNUGPL,检查kernel的版本是否支持b.函数调用规则:允许bpf函数之间的相互调用只允许调用kernel允许的BPFhelper函数,具体可以参考linux/bpf.h文件上述以外的函数及动态链接都是不允许的。c.流程处理规则:不允许使用loop循环以防止进入死循环卡死kernel不允许有不可到达的分支代码d.堆栈大小被限制在MAX_BPF_STACK范围内。e.编译的字节码大小被限制在BPF_COMPLEXITY_LIMIT_INSNS范围内。钩子挂载点,主要包括:另外在kernel的源代码中samples/bpf目录下有大量的示例,感兴趣的可以阅读下。三、eBPF在Android平台的使用经过上面枯燥的讲解,大家应该对eBPF有了基础的认识,下面我们就来通过android平台上的一个监控性能的小例子来实操下。这个小例子的需求是统计系统中每个应用在一段时间内系统调用的次数。3.1android系统对eBPF的编译支持目前android编译系统已经对eBPF进行了集成,通过android.bp就能很方便的在android源代码中编译eBPF的字节码。android.bp示例:相关的编译代码在soong的bpf.go,虽然google关于soong的文档很少,但是至少代码是比较清晰的。这里的$ccCmd一般是clang,所以它的编译命令主要是clang--target=bpf。和普通的bpf编译没有区别。3.2eBPF钩子代码实现解决了编译问题,下一步我们开始实现钩子代码,我们准备使用tracepoint钩子,首先要找到我们需要的tracepoint函数sys_enter和sys_exit。函数定义在include/trace/events/syscalls.h文件sys_enter的trace参数是id和长度为6的数组。sys_exit的trace参数是两个长整形数id和ret。找到了钩子后,下一步就可以编写钩子处理代码了:定义map保存系统调用统计信息,在DEFINE_BPF_MAP声明map的同时,也会生成删,改,查的宏函数,例如本例中会生成如下函数:bpf_pid_syscall_map_lookup_elembpf_pid_syscall_map_update_elembpf_pid_syscall_map_delete_elem定义回调函数参数类型,需要参考前面的tracepoint的定义。指定监听的tracepoint事件。使用bpf_trace_printk函数打印debug信息,会直接打印信息到ftrace中。在map中查找指定key。更新指定的key的值。3.3加载钩子代码我们只需要把我们编译出来的*.o文件push到手机的system/etc/bpf目录下,重启手机,系统会自动加载我们的钩子文件,加载成功后会在/sys/fs/bpf目录下显示我们定义的map及prog文件。系统加载代码在system/bpf/bpfloader中,代码很简单。主要有如下操作:1)在early-init阶段向下面两个节点写1–

/proc/sys/net/core/bpf_jit_enable使能eBPFJIT,当内核设定BPF_JIT_ALWAYS_ON的时候,默认为1–/proc/sys/net/core/bpf_jit_kallsyms使特权用户可以通过kallsyms节点读取kernel的symbols2)启动bpfloaderservice–读取system/etc/bpf目录下的*.o文件,调用libbpf_android.so中的loadProg函数加载进内核。–生成相应的/sys/fs/bpf/节点。–设置属性gs_loaded为1sys节点分为map节点和prog节点两种,分别为map_<filename>_,prog__下面是AndroidQ版本上的节点信息。可以使用下面的命令调试动态加载3.4用户空间程序实现下面我们需要编写用户空间的显示程序,本质上就是在用户态通过系统调用把BPFmap给读出来1)eBPF统计只有在调用bpf_attach_tracepoint只有才会起作用。bpf_attach_tracepoint是bcc里面的函数,android将bcc的一部分内容打包成了libbpf,放到了系统库里面。2)取得map的fd,bpf_obj_get会直接调用bpf的系统调用。3)将fd包装成BpfMap,android在BpfMap.h中定义了很多方便的函数。4)遍历map回调函数。返回值必须是android::netdutils::status::ok(在android的新版本中已经进行修改)。3.5运行结果查看直接在目录下执行mm,将编译出来的bpf.opush到/system/etc/bpf目录下,将统计程序push到/system/bin目录下,重启,看下结果。前面的是pid,后面的是系统调用次数。至此,如何在android平台使用eBPF实现统计系统中每个pid在一段时间内系统调用的次数的功能就介绍完了。此外还有很多技术细节没有深入研究,不过毕竟只是初探,就先讲到这里了,后续有时间再进一步深入研究。研究的时间还是比较短,如果有任何错误的地方欢迎指正。四、seccomp概述下面内容来自Linux官方文档:4.1历史seccomp首个版本在2005年合入Linux2.6.12版本。通过在/proc/PID/seccomp中写入1启用该功能。一旦启用,进程只能使用4个系统调用read(),write(),exit()和sigreturn(),如果进程调用其他系统调用将会导致SIGKILL。该想法和补丁来自andreaarcangeli,作为一种安全运行他人代码的方法。然而,这个想法一直没有实现。在2007年,内核2.6.23中改变了启用seccomp的方式。添加了prctl()操作方式(PR_SET_SECCOMP和SECCOMP_MODE_STRICT参数),并移除了/proc接口。PR_GET_SECCOMP操作的行为比较有趣:如果进程不处于seccomp模式,则会返回0,否则会发出SIGKILL信号(原因是prctl()不是一个允许的系统调用)。Kerrisk说,这证明了内核开发人员确实有幽默感。在接下来的五年左右,seccomp领域的情况一直很平静,直到2012年linux3.5中加入了seccomp模式2(或“seccomp过滤模式”)。为seccomp添加了第二个模式:SECCOMP_MODE_FILTER。使用该模式,进程可以指定允许哪些系统调用。通过mini的BPF程序,进程可以限制整个系统调用或特定的参数值。现在已经有很多工具使用了seccomp过滤,包括Chrome/Chromium浏览器,OpenSSH,vsftpd,和FirefoxOS。此外,容器中也大量使用了seccomp。2013年的3.8内核版主中,在/proc/PID/status中添加了一个“Seccomp”字段。通过读取该字段,进程可以确定其seccomp模式(0为禁用,1为严格,2为过滤)。Kerrisk指出,进程可能需要从其他地方获取一个文件的文件描述符,以确保不会收到SIGKILL。2014年3.17版本中加入了seccomp()系统调用(不会再使得prctl()系统调用变得更加复杂)。seccomp()系统调用提供了现有功能的超集。它还增加了将一个进程的所有线程同步到同一组过滤器的能力,有助于确保即使是在安装过滤器之前创建的线程也仍然受其影响。4.2BPFseccomp的过滤模式允许开发者编写BPF程序来根据传入的参数数目和参数值来决定是否可以运行某个给定的系统调用。只有值传递有效(BPF虚拟机不会取消对指针参数的引用)。可以使用seccomp()或prctl()安装过滤器。首先必须构造BPF程序,然后将其安装到内核。之后每次执行系统调用时都会触发过滤代码。也可以移除已经安装的过滤器(因为安装过滤器实际上是一种声明,表明任何后续执行的代码都是不可信的)。BPF语言几乎早于Linux(Kerrisk)。首次出现在1992年,被用于tcpdump程序,用于监听网络报文。但由于报文数目比较大,因此将所有的报文传递到用于空间再进行过滤的代价相当大。BPF提供了一种内核层面的过滤,这样用户空间只需要处理其感兴趣的报文。seccomp过滤器开发人员发现可以使用BPF实现其他类型的功能,后来BPF演化为允许过滤系统调用。内核中的小型内核内虚拟机用于解释一组简单的BPF指令。BPF允许分支,但仅允许向前的分支,因此不能出现循环,通过这种方式保证出现能够结束。BPF程序的指令限制为4096个,且在加载期间完成有效性校验。此外,校验器可以保证程序能够正常退出,并返回一条指令,告诉内核针对该系统调用应该采取何种动作。BPF的推广正在进行中,其中eBPF已经添加到了内核中,可以针对tracepoint(Linux3.18)和rawsocket(3.19)进行过滤,同时在4.1版本中合入了针对perfevent的eBPF代码。BPF有一个累加器寄存器,一个数据区(用于seccomp,包含系统调用的信息),以及一个隐式程序计数器。所有的指令都是64位长度,其中16比特用于操作码,两个8bit字段用于跳转目的地,以及一个32位的字段保存依赖操作码解析出的值。BPF使用的基本的指令有:load,stora,jump,算术和逻辑运算,以及return。BPF支持条件和非条件跳转指令,后者使用32位字段作为其偏移量。条件跳转会在指令中使用两个跳转目的字段,每个字段都包含一个跳转偏移量(具体取决于跳转为true还是false)。由于具有两个跳转目的,BPF可以简化条件跳转指令(例如,可以使用"等于时跳转",但不能使用"不等于时跳转"),如果需要另一种意义上的比较,可以将这两种偏移互换。目的地即是偏移量,0表示"不跳转"(执行下一跳指令),由于它们是8比特的值,最大支持跳转255条指令。正如前面所述,不允许负偏移量,避免循环。给seccomp使用的BPF数据区(structseccomp_data)有几个不同的字段来描述正在进行的系统调用:系统调用号,架构,指令指针,以及系统调用参数。它是一个只读buffer,程序无法修改。4.3编写过滤器可以使用常数和宏编写BPF程序,例如:BPF_STMT(BPF_LD

|

BPF_W

|

BPF_ABS,

(offsetof(struct

seccomp_data,

arch)))上述命令将会创建一个加载(BPF_LD)字(BPF_W)的操作,使用指令中的值作为数据区的偏移量(BPF_ABS)。该值是architecture字段与数据区域的偏移量,因此最终结果是一条指令,该指令会根据架构加载累加器(来自AUDIT.h中的AUDIT_ARCH_*值)。下一条指令为:BPF_JUMP(BPF_JMP

|

BPF_JEQ

|

BPF_K

,AUDIT_ARCH_X86_64

,

1,

0)上述命令会创建一个jump-if-equal指令(BPF_JMP|BPFJEQ),将指令中的值(BPF_K)与累加器中的值进行比较。如果架构为x86-64,该跳转会忽略吓一跳指令(跳转的指令数为"1"),否则会继续执行(跳转为false,"0")。BPF程序应该首先对其架构进行校验,确保系统调用与程序所期望的一致。BPF程序可能是在与它允许的架构不同的架构上创建的。一旦创建了过滤器,在每次系统调用时都会允许该程序,同时也会对性能造成一定影响。每个程序在退出时必须返回一条指令,否则,校验器会返回EINVAL。返回的内容为一个32位的数值。高16比特指定了内核的动作,其他比特返回与动作相关的数据。程序可以返回5个动作:SECCOMP_RET_ALLOW表示允许运行系统调用;SECCOMP_RET_KILL表示终止进程,就像该进程由于SIGSYS(进程不会捕获到该信号)被杀死一样;SECCOMP_RET_ERRNO会告诉内核尝试通知一个ptrace()跟踪器,使其有机会获得控制权;SECCOMP_RET_TRAP告诉内核立即发送一个真实的SIGSYS信号,进程会在期望时捕获到该信号。可以使用seccomp()(sinceLinux3.17)或prctl()安装BPF程序,这两种情况下都会传递一个structsock_fprog指针,包含指令数目和一个指向程序的指针。为了成功执行指令,调用者要么需要具有CAP_SYS_ADMIN权限,要么给进程设置PR_SET_NO_NEW_PRIVS属性(使用execve()执行新的程序时会忽略set-UID,set-GID,和文件capabilities)。如果过滤器运行程序调用prctl()或seccomp(),那么就可以安装更多的过滤器,它们将以与添加顺序相反的顺序运行,最终返回过滤器中具有最高优先级的值(KILL的优先级最高,ALLOW的优先级最低)。如果筛选器允许调用fork()、clone()和execve(),则会在调用这些命令时保留筛选器。seccomp过滤器的两个主要用途是沙盒和故障模式测试。前者用于限制程序,特别是需要处理不可信输入的系统调用,通常会用到白名单。对于故障模式测试,可以使用seccomp给程序注入各种不可预期的错误来帮助查找bugs。目前有很多工具和资源可以简化seccomp过滤器和BPF的开发。Libseccomp提供了一组高级API来创建过滤器。libseccomp项目给出了很多帮助文档,如seccomp_init()。最后,内核有一个just-in-time(JIT)编译器,用于将BPF字节码转化为机器码,通过这种方式可以提升2-3倍的性能。JIT编译器默认是禁用的,可以通过在下面文件中写入1启用。/proc/sys/net/core/bpf_jit_enable4.4XDP概述XDP是Linux网络路径上内核集成的数据包处理器,具有安全、可编程、高性能的特点。当网卡驱动程序收到数据包时,该处理器执行BPF程序。XDP可以在数据包进入协议栈之前就进行处理,因此具有很高的性能,可用于DDoS防御、防火墙、负载均衡等领域。XDP数据结构XDP程序使用的数据结构是xdp_buff,而不是sk_buff,xdp_buff可以视为sk_buff的轻量级版本。两者的区别在于:sk_buff包含数据包的元数据,xdp_buff创建更早,不依赖与其他内核层,因此XDP可以更快的获取和处理数据包。xdp_buff数据结构定义如下:///linux/include/net/xdp.h

structxdp_rxq_info{

structnet_device*dev;

u32queue_index;

u32reg_state;

structxdp_mem_infomem;

}____cacheline_aligned;/*perfcritical,avoidfalse-sharing*/

structxdp_buff{

void*data;

void*data_end;

void*data_meta;

void*data_hard_start;

unsignedlonghandle;

structxdp_rxq_info*rxq;

};sk_buff数据结构定义如下:///include/linux/skbuff.h

structsk_buff{

union{

struct{

/*Thesetwomembersmustbefirst.*/

structsk_buff *next;

structsk_buff *prev;

union{

structnet_device *dev;

/*Someprotocolsmightusethisspacetostoreinformation,

*whiledevicepointerwouldbeNULL.

*UDPreceivepathisoneuser.

*/

unsignedlong dev_scratch;

};

};

structrb_node rbnode;/*usedinnetem,ip4defrag,andtcpstack*/

structlist_head list;

};

union{

structsock *sk;

int ip_defrag_offset;

};

union{

ktime_t tstamp;

u64 skb_mstamp_ns;/*earliestdeparturetime*/

};

/*

*Thisisthecontrolbuffer.Itisfreetouseforevery

*layer.Pleaseputyourprivatevariablesthere.Ifyou

*wanttokeepthemacrosslayersyouhavetodoaskb_clone()

*first.ThisisownedbywhoeverhastheskbqueuedATM.

*/

char cb[48]__aligned(8);

union{

struct{

unsignedlong _skb_refdst;

void (*destructor)(structsk_buff*skb);

};

structlist_head tcp_tsorted_anchor;

};

#ifdefined(CONFIG_NF_CONNTRACK)||defined(CONFIG_NF_CONNTRACK_MODULE)

unsignedlong _nfct;

#endif

unsignedint len,

data_len;

__u16 mac_len,

hdr_len;

/*Followingfieldsare_not_copiedin__copy_skb_header()

*Notethatqueue_mappingisheremostlytofillahole.

*/

__u16 queue_mapping;

/*ifyoumoveclonedaroundyoualsomustadaptthoseconstants*/

#ifdef__BIG_ENDIAN_BITFIELD

#defineCLONED_MASK (1<<7)

#else

#defineCLONED_MASK 1

#endif

#defineCLONED_OFFSET() offsetof(structsk_buff,__cloned_offset)

__u8 __cloned_offset[0];

__u8 cloned:1,

nohdr:1,

fclone:2,

peeked:1,

head_frag:1,

xmit_more:1,

pfmemalloc:1;

#ifdefCONFIG_SKB_EXTENSIONS

__u8 active_extensions;

#endif

/*fieldsenclosedinheaders_start/headers_endarecopied

*usingasinglememcpy()in__copy_skb_header()

*/

/*private:*/

__u32 headers_start[0];

/*public:*/

/*ifyoumovepkt_typearoundyoualsomustadaptthoseconstants*/

#ifdef__BIG_ENDIAN_BITFIELD

#definePKT_TYPE_MAX (7<<5)

#else

#definePKT_TYPE_MAX 7

#endif

#definePKT_TYPE_OFFSET() offsetof(structsk_buff,__pkt_type_offset)

__u8 __pkt_type_offset[0];

__u8 pkt_type:3;

__u8 ignore_df:1;

__u8 nf_trace:1;

__u8 ip_summed:2;

__u8 ooo_okay:1;

__u8 l4_hash:1;

__u8 sw_hash:1;

__u8 wifi_acked_valid:1;

__u8 wifi_acked:1;

__u8 no_fcs:1;

/*Indicatestheinnerheadersarevalidintheskbuff.*/

__u8 encapsulation:1;

__u8 encap_hdr_csum:1;

__u8 csum_valid:1;

#ifdef__BIG_ENDIAN_BITFIELD

#definePKT_VLAN_PRESENT_BIT 7

#else

#definePKT_VLAN_PRESENT_BIT 0

#endif

#definePKT_VLAN_PRESENT_OFFSET() offsetof(structsk_buff,__pkt_vlan_present_offset)

__u8 __pkt_vlan_present_offset[0];

__u8 vlan_present:1;

__u8 csum_complete_sw:1;

__u8 csum_level:2;

__u8 csum_not_inet:1;

__u8 dst_pending_confirm:1;

#ifdefCONFIG_IPV6_NDISC_NODETYPE

__u8 ndisc_nodetype:2;

#endif

__u8 ipvs_property:1;

__u8 inner_protocol_type:1;

__u8 remcsum_offload:1;

#ifdefCONFIG_NET_SWITCHDEV

__u8 offload_fwd_mark:1;

__u8 offload_l3_fwd_mark:1;

#endif

#ifdefCONFIG_NET_CLS_ACT

__u8 tc_skip_classify:1;

__u8 tc_at_ingress:1;

__u8 tc_redirected:1;

__u8 tc_from_ingress:1;

#endif

#ifdefCONFIG_TLS_DEVICE

__u8 decrypted:1;

#endif

#ifdefCONFIG_NET_SCHED

__u16 tc_index; /*trafficcontrolindex*/

#endif

union{

__wsum csum;

struct{

__u16 csum_start;

__u16 csum_offset;

};

};

__u32 priority;

int skb_iif;

__u32 hash;

__be16 vlan_proto;

__u16 vlan_tci;

#ifdefined(CONFIG_NET_RX_BUSY_POLL)||defined(CONFIG_XPS)

union{

unsignedint napi_id;

unsignedint sender_cpu;

};

#endif

#ifdefCONFIG_NETWORK_SECMARK

__u32 secmark;

#endif

union{

__u32 mark;

__u32 reserved_tailroom;

};

union{

__be16 inner_protocol;

__u8 inner_ipproto;

};

__u16 inner_transport_header;

__u16 inner_network_header;

__u16 inner_mac_header;

__be16 protocol;

__u16 transport_header;

__u16 network_header;

__u16 mac_header;

/*private:*/

__u32 headers_end[0];

/*public:*/

/*Theseelementsmustbeattheend,seealloc_skb()fordetails.*/

sk_buff_data_t tail;

sk_buff_data_t end;

unsignedchar *head,

*data;

unsignedint truesize;

refcount_t users;

#ifdefCONFIG_SKB_EXTENSIONS

/*onlyuseableafterchecking->active_extensions!=0*/

structskb_ext *extensions;

#endif

};4.5XDP与eBPF的关系XDP程序是通过bpf()系统调用控制的,bpf()系统调用使用程序类型BPF_PROG_TYPE_XDP进行加载。XDP操作模式XDP支持3种工作模式,默认使用native模式:NativeXDP:在native模式下,XDPBPF程序运行在网络驱动的早期接收路径上(RX队列),因此,使用该模式时需要网卡驱动程序支持。OffloadedXDP:在Offloaded模式下,XDPBFP程序直接在NIC(NetworkInterfaceController)中处理数据包,而不使用主机CPU,相比native模式,性能更高GenericXDP:Generic模式主要提供给开发人员测试使用,对于网卡或驱动无法支持native或offloaded模式的情况,内核提供了通用的generic模式,运行在协议栈中,不需要对驱动做任何修改。生产环境中建议使用native或offloaded模式XDP操作结果码XDP_DROP:丢弃数据包,发生在驱动程序的最早RX阶段XDP_PASS:将数据包传递到协议栈处理,操作可能为以下两种形式:1、正常接收数据包,分配愿数据sk_buff结构并且将接收数据包入栈,然后将数据包引导到另一个CPU进行处理。他允许原始接口到用户空间进行处理。这可能发生在数据包修改前或修改后。2、通过GRO(Genericreceiveoffload)方式接收大的数据包,并且合并相同连接的数据包。经过处理后,GRO最终将数据包传入“正常接收”流XDP_TX:转发数据包,将接收到的数据包发送回数据包到达的同一网卡。这可能在数据包修改前或修改后发生XDP_REDIRECT:数据包重定向,XDP_TX,XDP_REDIRECT是将数据包送到另一块网卡或传入到BPF的cpumap中

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论