BPF(Berkeley Packet Filter)是一种虚拟机指令集,最初用于网络数据包过滤,后来被扩展为eBPF(extended BPF)并广泛应用于Linux内核的各个子系统。下面将详细介绍BPF汇编语言并通过多个示例展示其用法。
BPF汇编是一种基于寄存器的汇编语言,主要特点包括:
11个64位寄存器:r0-r10
r0用于存储返回值,r10是栈帧指针
支持加ZAI(load)/存储、算术运算、跳转等指令
每条指令固定为8字节长度
基本指令格式
op:16, dst_reg:4, src_reg:4, off:16, imm:32
op:操作码
dst_reg:目标寄存器
src_reg:源寄存器
off:偏移量
imm:立即数
BPF汇编示例详解
示例1:简单的返回值
// 返回常量值42
mov r0, 42 // 将立即数42存入r0寄存器
exit // 退出程序,返回r0的值
这个最简单的BPF程序只是返回常量值42。mov指令将立即数移动到寄存器,exit指令结束程序执行。
示例2:条件判断
// 如果第一个参数大于10,返回1,否则返回0
ldxw r0, [r1+0] // 从r1指向的内存加ZAI(load)32位值到r0 (r1是第一个参数)
jgt r0, 10, 1 // 如果r0 > 10,跳过下一条指令
mov r0, 0 // 返回0
exit
mov r0, 1 // 返回1
exit
这个程序展示了条件跳转:
ldxw从第一个参数加ZAI(load)32位值
jgt进行大于比较,条件满足则跳过下一条指令
根据比较结果返回0或1
示例3:数组元素访问
// 访问数组的第二个元素并返回
mov r2, 1 // 数组索引1 (第二个元素)
ldxdw r0, [r1+r28] // 从基址r1 + 索引r28加ZAI(load)64位值
exit
这个示例展示了:
数组索引计算(r2*8,因为64位元素大小为8字节)
基址加偏移的内存访问模式
示例4:循环求和
// 对数组前5个元素求和
mov r0, 0 // 初始化累加器r0=0
mov r2, 0 // 初始化索引r2=0
loop:
jge r2, 5, exit_loop // 如果r2 >= 5,跳出循环
ldxdw r3, [r1+r2*8] // 加ZAI(load)array[r2]
add r0, r3 // r0 += array[r2]
add r2, 1 // r2++
ja loop // 无条件跳转回循环开始
exit_loop:
exit
这个更复杂的示例展示了:
循环结构实现
使用jge进行循环条件检查
ja无条件跳转
数组遍历和累加操作
示例5:哈希表查找
// 假设r1指向哈希表,r2是键,查找对应的值
mov r0, 0 // 默认返回0(未找到)
mov r3, 0 // 初始化哈希索引
hash_loop:
mov r4, [r1+r3*16+0] // 加ZAI(load)键
jeq r4, r2, found // 如果键匹配,跳转到found
add r3, 1 // 递增索引
jlt r3, 10, hash_loop // 如果r3 < 10继续循环
exit // 未找到,返回0
found:
ldxdw r0, [r1+r3*16+8] // 加ZAI(load)对应的值
exit
这个示例展示了:
哈希表遍历(假设每个条目16字节:8字节键+8字节值)
嵌套的条件跳转
更复杂的内存访问模式
BPF指令主要分为以下几类:
加ZAI(load)/存储指令
ldxb, ldxh, ldxw, ldxdw:从内存加ZAI(load)不同大小的数据
stb, sth, stw, stdw:存储数据到内存
lddw:加ZAI(load)64位立即数到寄存器
算术运算指令
add, sub, mul, div:加减乘除
and, or, xor, lsh, rsh, arsh:位操作
neg:取负
mov:寄存器移动
跳转指令
ja:无条件跳转
jeq, jne, jgt, jge, jlt, jle:条件跳转
jset:测试位跳转
call:函数调用
exit:退出程序
其他指令
be16, be32, be64:字节序转换
le16, le32, le64:小端序转换
BPF汇编编程技巧
寄存器使用:
r0:返回值
r1-r5:函数参数/临时寄存器
r6-r9:被调用者保存寄存器
r10:栈帧指针
内存访问:
必须检查边界,否则会被验证器拒绝
使用r10访问栈空间(如[r10-8])
循环结构:
必须有明确的退出条件
循环次数通常需要有限制
总结
BPF汇编虽然看起来简单,但能表达复杂的过滤和监控逻辑。通过组合基本指令,可以实现各种内核态的高效程序。现代eBPF还支持更多高级特性如映射访问、辅助函数调用等,但底层仍然是基于这种汇编指令集。