如今eBPF程序的编写,很多都是基于bcc或者bpftrace进行,也有开发者直接基于libbpf库进行,但是不管怎样,编写的xx.bpf.c程序,在加载到内核时,都必须经过内核的verifier校验器进行各种边界和内存检查,经常会碰到各种奇奇怪怪的 verifier 报错,导致 eBPF 程序加载失败。
有些错误,开发者可能要花费大量的时间去分析并修改程序,并祈祷程序能够加载成功。特别是在低版本的内核运行低版本Clang编译器编译的eBPF程序,错误提示非常糟糕,经常找不到出错点,这就大大增加了开发难度。
为此,本文梳理了一些常见的 eBPF verifier 报错,避免更多的人走弯路,写出能成功加载的 eBPF 程序。同时,本文通过讲解 eBPF verifier 检查原理及给出示例程序,来分析为什么 eBPF verifier 会报错,使读者能够知其然知其所以然,达到融会贯通。

eBPF verifier 是一个位于内核的校验器,用于验证 eBPF 程序的安全性,保证 eBPF 程序不会破坏内核,导致内核崩溃。对于一个 eBPF 程序, verifier 会对其进行两次检查( first pass 和 second pass)(暂且这么翻译)。
eBPF verifier 通过 dfs 算法检查程序是否为有向无环图(DAG)。在此阶段,以下几种情况的 eBPF 程序将会被 verifier 拒绝:
其中,第 1 种场景在 4.19 版本内核很少遇见,因为从该版本开始,指令数限制为 1000000 条。第 3 和第 4 两种场景,大部分开发者很少遇见。因为开发者都是使用高级语言编程,由编译器负责生成相应指令,所以一般不会产生 unreachable 指令和非法 jump。但是如果直接使用 eBPF 指令来写 eBPF 程序则有可能遇到此类问题。
因为指令回跳会增加指令分析的复杂度,所以 verifier 直接禁止出现指令回跳。下面是一个使用 for 循环引入指令回跳的场景:
SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
int i;
for (i=0;i<1000;i++)
bpf_printk("%d\n", i);
return 0;
}
上述eBPF代码在运行时,会报 back-edge from insn 12 to 2 错误。这个报错意思是 eBPF 程序的第 12 条指令跳转到第 2 条指令,形成会跳,即存在环。具体可以看下面的指令信息:
// bpftool
int tcp_sendmsg(struct pt_regs * ctx):
; int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
0: (b7) r6 = 0
1: (b7) r7 = 680997
; bpf_printk("%d\n", i);
2: (63) *(u32 *)(r10 -4) = r7
3: (bf) r1 = r10
;
4: (07) r1 += -4
; bpf_printk("%d\n", i);
5: (b7) r2 = 4
6: (bf) r3 = r6
7: (85) call bpf_trace_printk#-57568
; for (i=0;i<1000;i++)
8: (07) r6 += 1
9: (bf) r1 = r6
10: (67) r1 <<= 32
11: (77) r1 >>= 32
; for (i=0;i<1000;i++)
12: (55) if r1 != 0x3e8 goto pc-11
; int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
13: (b7) r0 = 0
14: (95) exit
对于该错误的一般解决方法是:在 for 循环前面添加 #pragma unroll,进行循环展开,避免指令回跳。
注意:在5.10内核版本是支持有限循环的,所以上述代码是可以在 5.10 内核正常运行。
verfier 第二次检查会遍历所有的分支,并记录寄存器状态。在此阶段,以下几种情况的 eBPF 程序将会被 verifier 拒绝:
栈访问非法
栈访问也是开发者写代码经常碰到的问题。
1、栈限制512字节
因为 verifier 会保存栈内存的状态,所以栈的大小是有限的,目前是 512 字节。当栈内存大小超过 512 字节时,则会被 verifier 拒绝。
SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
#define MAX_NUM (512/8)
volatile u64 arr[MAX_NUM + 1] = {};
arr[MAX_NUM] = 0xff;
bpf_printk("%lld\n", arr[MAX_NUM]);
return 0;
}
上述程序在编译阶段会报错:
Looks like the BPF stack limit of 512 bytes is exceeded. Please move large on stack variables into BPF per-cpu array map。
对于该错误,一般建议使用 map 来存储大数据。
注:因为是高级语言编程,所以这种容易检查出来的异常,编译器就直接报错了。如果直接使用 eBPF 指令,那么会由 verifier 拒绝程序的加载。
2、栈偏移仅支持常量
当访问栈时采用变量偏移,会导致无法推测寄存器的状态。所以 4.19 版本只支持常量偏移。下面是使用变量偏移的错误示例:
SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
u64 volatile arr[16] = {};
arr[bpf_ktime_get_ns() & 0xf] = 0;
return 0;
}
当执行该程序时,会报如下错误:
variable stack access var_off=(0x0; 0x78) off=-128 size=8-。
对于该错误的一般解决方法是将 arr 数组保存到 map 里面。
注意:5.10内核已经支持变量类型的栈偏移。
3、helper 函数参数类型不匹配
verfier 会检查 eBPF 程序中所调用 helper 函数的参数类型,比如 bpf_map_lookup_elem helper 函数的参数类型约束定义如下:
const struct bpf_func_proto bpf_map_lookup_elem_proto = {
.func = bpf_map_lookup_elem,
.gpl_only = false,
.pkt_access = true,
.ret_type = RET_PTR_TO_MAP_VALUE_OR_NULL,
.arg1_type = ARG_CONST_MAP_PTR, /* const argument used as pointer to bpf_map */
.arg2_type = ARG_PTR_TO_MAP_KEY, /* pointer to stack used as map key */
};
所以对于 bpf_map_lookup_elem helper 函数来说,其参数 2 类型约束为 ARG_PTR_TO_MAP_KEY,其表示指向栈的指针,即第二个参数的值必须存储在栈上。下面是一个错误的示例:
struct
{
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, struct sock *);
__type(value, struct sockmap_val);
__uint(max_entries, 1024);
} sockmap SEC(".maps");
struct sockmap_val
{
int nothing;
};
SEC("tracepoint/tcp/tcp_rcv_space_adjust")
int tp__tcp_rcv_space_adjust(struct trace_event_raw_tcp_event_sk *ctx)
{
struct sockmap_val *sv = bpf_map_lookup_elem(&sockmap, &ctx->skaddr);
if (sv)
bpf_printk("%d\n", sv->nothing);
return 0;
}
该程序会报错:R2 type=ctx expected=fp, pkt, pkt_meta, map_value。因为此时传的参数 ctx->skaddr 是 ctx 类型参数,非 fp 类型。对于该问题的一般解决方法是:定义一个栈变量,将 ctx->skaddr 的值存在栈上,即 u64 skaddr = ctx->skaddr 。
4、未做范围检查
范围检查主要是用来判断内存访问是否越界。
struct
{
__uint(type, BPF_MAP_TYPE_HASH);
__type(key, int);
__type(value, int);
__uint(max_entries, 1024);
} indexmap SEC(".maps");
SEC("kprobe/tcp_sendmsg")
int BPF_KPROBE(tcp_sendmsg, struct sock *sk)
{
int map_key = 0;
int map_val = 0;
int array[10] = {};
int *map_val_ptr = bpf_map_look_up(&indexmap, &map_key);
if (map_val_ptr)
map_val = *map_val_ptr;
bpf_printk("array[%d] = %d\n", map_val, array[map_val]);
return 0;
}
char LICENSE[] SEC("license") = "GPL";
上述程序会报错:math between fp pointer and register with unbounded min value is not allowed。一般解决方法是:在内存访问时,进行范围检查。
虽然 eBPF 程序可以采用 c 语言编写,但是相比于 c 语言的非安全性,eBPF 则通过严格的检查来保证 eBPF 程序安全,与此同时也引入了大量的约束条件。
另外在写eBPF程序时,也会遇到其他的错误,比如不能去trace类似这样的函数,ip_rcv_finish_core.isra.16,它会作为perf trace事件的一个event name,而由于带了"."特殊符号,内核会检查不通过,导致运行失败。该问题在高版本内核已修复。

还有一种错误是运行时libbpf未能加载有效的btf文件,如下图所示。造成这个问题的原因是/boot 路径下的btf默认只支持elf格式。解决这类问题的方法是升级libbpf,高版本libbpf中支持两种格式,也可以通过libbpf参数指定btf路径来解决。

通过以上具体的例子介绍了常见的 verifier 报错,那么编写eBPF程序有哪些需要注意的地方呢?以下为通过实战开发经验,总结出来的一些tips(仅供参考,可能内核版本及libbpf版本不一样结果有所差异,如果需要可以自行再验证一下):
PTR_TO_CTX,PTR_TO_MAP, PTR_TO_STACK)PTR_TO_CTX,PTR_TO_MAP, PTR_TO_STACK),对访问的大小/对齐/边界进行校验1<<(KMALLOC_SHIFT_MAX-1)RLIMIT_MEMLOCK的限制ptr && ptr , int && intbpf_probe_read 的dst需要是栈空间bpf_get_current_comm的dst需要是栈空间bpf_map_update_elem参数都要是栈空间bpf_map_lookup_elem的返回做检测bpf_probe_read即使开发者再怎么注意,也难免出现BUG和编译错误,如果当运行时出错提示如果能够更精确一些,明确告知异常代码在哪一行,那将是非常方便的。前面这些tips和错误解决方法都是经过不断的开发实践得来的经验总结,平时大家在编码时多认真review,最主要的还是多写多去用方可写出高效代码。
大约一年前,我决定确保每个包含非唯一文本的Flash通知都将从模块中的方法中获取文本。我这样做的最初原因是为了避免一遍又一遍地输入相同的字符串。如果我想更改措辞,我可以在一个地方轻松完成,而且一遍又一遍地重复同一件事而出现拼写错误的可能性也会降低。我最终得到的是这样的:moduleMessagesdefformat_error_messages(errors)errors.map{|attribute,message|"Error:#{attribute.to_s.titleize}#{message}."}enddeferror_message_could_not_find(obje
我遵循MichaelHartl的“RubyonRails教程:学习Web开发”,并创建了检查用户名和电子邮件长度有效性的测试(名称最多50个字符,电子邮件最多255个字符)。test/helpers/application_helper_test.rb的内容是:require'test_helper'classApplicationHelperTest在运行bundleexecraketest时,所有测试都通过了,但我看到以下消息在最后被标记为错误:ERROR["test_full_title_helper",ApplicationHelperTest,1.820016791]test
我是rails的新手,想在form字段上应用验证。myviewsnew.html.erb.....模拟.rbclassSimulation{:in=>1..25,:message=>'Therowmustbebetween1and25'}end模拟Controller.rbclassSimulationsController我想检查模型类中row字段的整数范围,如果不在范围内则返回错误信息。我可以检查上面代码的范围,但无法返回错误消息提前致谢 最佳答案 关键是您使用的是模型表单,一种显示ActiveRecord模型实例属性的表单。c
我正在尝试编写一个将文件上传到AWS并公开该文件的Ruby脚本。我做了以下事情:s3=Aws::S3::Resource.new(credentials:Aws::Credentials.new(KEY,SECRET),region:'us-west-2')obj=s3.bucket('stg-db').object('key')obj.upload_file(filename)这似乎工作正常,除了该文件不是公开可用的,而且我无法获得它的公共(public)URL。但是当我登录到S3时,我可以正常查看我的文件。为了使其公开可用,我将最后一行更改为obj.upload_file(file
我克隆了一个rails仓库,我现在正尝试捆绑安装背景:OSXElCapitanruby2.2.3p173(2015-08-18修订版51636)[x86_64-darwin15]rails-v在您的Gemfile中列出的或native可用的任何gem源中找不到gem'pg(>=0)ruby'。运行bundleinstall以安装缺少的gem。bundleinstallFetchinggemmetadatafromhttps://rubygems.org/............Fetchingversionmetadatafromhttps://rubygems.org/...Fe
在Cooper的书BeginningRuby中,第166页有一个我无法重现的示例。classSongincludeComparableattr_accessor:lengthdef(other)@lengthother.lengthenddefinitialize(song_name,length)@song_name=song_name@length=lengthendenda=Song.new('Rockaroundtheclock',143)b=Song.new('BohemianRhapsody',544)c=Song.new('MinuteWaltz',60)a.betwee
我是Google云的新手,我正在尝试对其进行首次部署。我的第一个部署是RubyonRails项目。我基本上是在关注thisguideinthegoogleclouddocumentation.唯一的区别是我使用的是我自己的项目,而不是他们提供的“helloworld”项目。这是我的app.yaml文件runtime:customvm:trueentrypoint:bundleexecrackup-p8080-Eproductionconfig.ruresources:cpu:0.5memory_gb:1.3disk_size_gb:10当我转到我的项目目录并运行gcloudprevie
我有两个Rails模型,即Invoice和Invoice_details。一个Invoice_details属于Invoice,一个Invoice有多个Invoice_details。我无法使用accepts_nested_attributes_forinInvoice通过Invoice模型保存Invoice_details。我收到以下错误:(0.2ms)BEGIN(0.2ms)ROLLBACKCompleted422UnprocessableEntityin25ms(ActiveRecord:4.0ms)ActiveRecord::RecordInvalid(Validationfa
这个问题在这里已经有了答案:Arraysmisbehaving(1个回答)关闭6年前。是否应该这样,即我误解了,还是错误?a=Array.new(3,Array.new(3))a[1].fill('g')=>[["g","g","g"],["g","g","g"],["g","g","g"]]它不应该导致:=>[[nil,nil,nil],["g","g","g"],[nil,nil,nil]]
尝试在我的RoR应用程序中实现计数器缓存列时出现错误Unknownkey(s):counter_cache。我在这个问题中实现了模型关联:Modelassociationquestion这是我的迁移:classAddVideoVotesCountToVideos0Video.reset_column_informationVideo.find(:all).eachdo|p|p.update_attributes:videos_votes_count,p.video_votes.lengthendenddefself.downremove_column:videos,:video_vot