eBPF - bpf2go에서 전역 변수 설정 방법
전에 Global variables를 언급만 했었는데요,
eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE)
- Application configuration
; https://www.sysnet.pe.kr/2/0/13801#10
bpf2go의 현재(2024-11-13) 문서와는 달리,
ebpf-go Documentation - Global Variables
; https://ebpf-go.dev/concepts/global-variables/#runtime-constants
예제 코드의 CollectionSpec.Variables 변수 자체가 없기 때문에 실습이 안 됐었습니다.
// Load the object file from disk using a bpf2go-generated scaffolding.
spec, err := loadVariables()
if err != nil {
panicf("loading CollectionSpec: %s", err)
}
// Set the 'const_u32' variable to 42 in the CollectionSpec.
want := uint32(42)
if err := spec.Variables["const_u32"].Set(want); err != nil {
panicf("setting variable: %s", err)
}
// Load the CollectionSpec.
//
// Note: modifying spec.Variables after this point is ineffectual!
// Modifying *Spec resources does not affect loaded/running BPF programs.
var obj variablesPrograms
if err := spec.LoadAndAssign(&obj, nil); err != nil {
panicf("loading BPF program: %s", err)
}
그래도 혹시나 싶어, CollectionSpec 타입을 살펴봤더니 RewriteConstants 함수가 나오는데요, 왠지 저게 맞는 것 같습니다. 이걸로 실습을 해볼까요? ^^
자, 우선 전역 변수를 정의한 eBPF 코드를 작성하고,
// basic.c
#include "vmlinux.h"
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#include <bpf/bpf_core_read.h>
volatile const __u32 const_u32 = 50;
SEC("tracepoint/syscalls/sys_enter_close")
int sys_enter_close(struct trace_event_raw_sys_enter *ctx) {
bpf_printk("sys_enter_close called: %d", const_u32);
return 0;
}
빌드합니다.
// go:generate go run github.com/cilium/ebpf/cmd/bpf2go -target amd64 ebpf_basic basic.c
이제 go 코드로 연동을 해야 하는데요,
지난번에 작성한 기본 예제 코드에서는 (위의 bpf2go로 자동 생성된) loadEbpf_basicObjects을 이용했지만 이제는 RewriteConstants 함수를 사용하기 위해 CollectionSpec을 직접 로드하는 것으로 시작해야 합니다.
// CollectionSpec을 먼저 로드
spec, err := loadEbpf_basic() // bpf2go로 자동 생성된 함수
if err != nil {
fmt.Printf("can't load ebpf_basic: %v\n", err)
return
}
이후, RewriteConstants 함수를 이용하여 전역 변수를 설정하고,
defaultConstValue := uint32(12)
err = spec.RewriteConstants(map[string]interface{}{ // bpf2go로 자동 생성된 함수
"const_u32": defaultConstValue,
})
이제서야 CollectionSpec을 이용해 eBPF 코드를 로드하면 됩니다.
opts := ebpf.CollectionOptions{
Programs: ebpf.ProgramOptions{},
Maps: ebpf.MapOptions{},
}
var bpfObj ebpf_basicObjects
err = spec.LoadAndAssign(&bpfObj, &opts) // bpf2go로 자동 생성된 함수
if err != nil {
fmt.Printf("load: objs == null, %v\n", err)
return
}
이후 실행해 로그를 살펴보면,
$ sudo cat /sys/kernel/debug/tracing/trace_pipe
...[생략]...
node-350690 [002] ...21 204126.409772: bpf_trace_printk: sys_enter_close called: 1
...[생략]...
저렇게 const_u32의 값이 (50이 아닌) 1로 나오는 것을 확인할 수 있습니다.
참고로, go 언어 측에서 전역 변수의 값을 확인하는 것도 가능합니다. 이를 위해 값을 확인하는 용도의 함수를 하나 추가해 주고,
// basic.c
// ...[생략]...
SEC("socket") int const_example() {
return const_u32;
}
go 코드에서 spec.LoadAndAssign 호출 후 bpf2go가 위의 const_example 함수에 대해 자동으로 생성한 코드를 이용하면,
// Note: the kernel expects at least 14 bytes input for an ethernet header for
// XDP and SKB programs.
ret, dataOut, err := bpfObj.ConstExample.Test(make([]byte, 14)) // ConstExample은 bpf2go로 자동 생성된 코드
if err != nil || ret != defaultConstValue {
fmt.Printf("ConstExample.Test failed: %v, %v\n", err, dataOut)
return
}
정상적으로 코드가 적용됐는지 확인할 수 있습니다. 위에서 Test 함수도 자동 생성된 코드인데요,
// Test runs the Program in the kernel with the given input and returns the
// value returned by the eBPF program.
//
// Note: the kernel expects at least 14 bytes input for an ethernet header for
// XDP and SKB programs.
//
// This function requires at least Linux 4.12.
func (p *Program) Test(in []byte) (uint32, []byte, error) {
// Older kernels ignore the dataSizeOut argument when copying to user space.
// Combined with things like bpf_xdp_adjust_head() we don't really know what the final
// size will be. Hence we allocate an output buffer which we hope will always be large
// enough, and panic if the kernel wrote past the end of the allocation.
// See https://patchwork.ozlabs.org/cover/1006822/
var out []byte
if len(in) > 0 {
out = make([]byte, len(in)+outputPad)
}
opts := RunOptions{
Data: in,
DataOut: out,
Repeat: 1,
}
ret, _, err := p.run(&opts)
if err != nil {
return ret, nil, fmt.Errorf("test program: %w", err)
}
return ret, opts.DataOut, nil
}
Test의 인자로 14바이트의 빈 배열을 넘겨주는 것에 대해 XDP와 SKB 유형의 경우에 ethernet header를 위한 최소 공간이라는 주석이 나옵니다. 그렇다면 제가 만든 eBPF 코드는 단순히 tracepoint를 이용한 것이므로 저런 제약이 없을 것 같은데요, 하지만 14바이트 미만으로 버퍼를 넘기게 되면,
// 아래의 모든 호출은 오류 반환
bpfObj.ConstExample.Test(make([]byte, 13))
bpfObj.ConstExample.Test(make([]byte, 0))
bpfObj.ConstExample.Test(nil)
error 반환 값으로 "test program: invalid argument, []" 메시지가 나옵니다. 게다가 outputPad 만큼의 out 버퍼 공간을 in 버퍼 공간에 더해 만들어 주는 것도,
var out []byte
if len(in) > 0 {
out = make([]byte, len(in)+outputPad)
}
꽤나 이해하기 힘든 코드입니다. ^^; 어쨌든, Test 함수의 골격을 봤으니 우리가 직접 이렇게 만들어 호출하는 것도 가능합니다.
inData := make([]byte, 14)
outputPad := 256 + 2
outData := make([]byte, 14+outputPad)
runOpts := ebpf.RunOptions{
Data: inData,
DataOut: outData,
Repeat: 1,
}
ret, err := bpfObj.ConstExample.Run(&runOpts)
if err != nil || ret != defaultConstValue {
fmt.Printf("ConstExample.Test failed: %v\n", err)
return
}
예제로 만든 const_example의 Section을 "socket"으로 정했는데요, 혹시나 싶어 다른 걸로 넣어봤더니,
SEC("tracepoint") int const_example() {
return const_u32;
}
실행 시 go 측의 ConstExample.Run에서 오류가 발생합니다.
run program: kernel doesn't support running TracePoint: not supported
어차피 User-space 측에서 호출하는 건데, 왜 socket과 tracepoint의 차이가 발생하는지는 잘 모르겠습니다. ^^; 대충 몇 개 더 해봤는데,
SEC("test") - field ConstExample: cannot load program const_example: program type is unspecified
SEC("kprobe") - run program: kernel doesn't support running Kprobe: not supported
SEC("raw_tracepoint") - run program: invalid argument
SEC("sk_skb") - run program: kernel doesn't support running SkSKB: not supported
SEC("xdp") OK
뭔가 규칙을 알 수가 없군요. 이 분야도 나름 역사가 쌓인지라 관련해서 이력을 알아내기가 쉽지 않은데요, 혹시 아시는 분은 덧글 부탁드립니다. ^^
일단, RewriteConstants로 전역 const 변수는 해결을 했는데요, 여전히
(non-const) 전역 변수는 구현이 안 됩니다.
문서의 예제에는 spec.Variables로 직접 접근하고 있는데,
// eBPF 코드 예제
volatile __u16 global_u16;
SEC("socket") int global_example() {
global_u16++;
return global_u16;
}
// go 코드 예제
set := uint16(9000)
if err := spec.Variables["global_u16"].Set(set); err != nil {
panicf("setting variable: %s", err)
}
현재 저 변수가 제공되지 않는 데다, RewriteConstants처럼 유사한 다른 함수도 없어 방향이 안 잡힙니다. 아무래도 이것은
bpf2go의 업데이트를 기다려야 할 것 같습니다. ^^ (혹은,
Map을 이용해도 됩니다.)
[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]