Microsoft MVP성태의 닷넷 이야기
Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법 [링크 복사], [링크+제목 복사],
조회: 4799
글쓴 사람
정성태 (seongtaejeong at gmail.com)
홈페이지
첨부 파일
 
(연관된 글이 1개 있습니다.)
(시리즈 글이 14개 있습니다.)
Linux: 86. Golang + bpf2go를 사용한 eBPF 기본 예제
; https://www.sysnet.pe.kr/2/0/13769

Linux: 94. eBPF - vmlinux.h 헤더 포함하는 방법 (bpf2go에서 사용)
; https://www.sysnet.pe.kr/2/0/13783

Linux: 95. eBPF - kprobe를 이용한 트레이스
; https://www.sysnet.pe.kr/2/0/13784

Linux: 96. eBPF (bpf2go) - fentry, fexit를 이용한 트레이스
; https://www.sysnet.pe.kr/2/0/13788

Linux: 100.  eBPF의 2가지 방식 - libbcc와 libbpf(CO-RE)
; https://www.sysnet.pe.kr/2/0/13801

Linux: 103. eBPF (bpf2go) - Tracepoint를 이용한 트레이스 (BPF_PROG_TYPE_TRACEPOINT)
; https://www.sysnet.pe.kr/2/0/13810

Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법
; https://www.sysnet.pe.kr/2/0/13815

Linux: 106. eBPF / bpf2go - (BPF_MAP_TYPE_HASH) Map을 이용한 전역 변수 구현
; https://www.sysnet.pe.kr/2/0/13817

Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
; https://www.sysnet.pe.kr/2/0/13819

Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
; https://www.sysnet.pe.kr/2/0/13824

Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
; https://www.sysnet.pe.kr/2/0/13825

Linux: 115. eBPF (bpf2go) - ARRAY / HASH map 기본 사용법
; https://www.sysnet.pe.kr/2/0/13893

Linux: 116. eBPF / bpf2go - BTF Style Maps 정의 구문과 데이터 정렬 문제
; https://www.sysnet.pe.kr/2/0/13894

Linux: 117. eBPF / bpf2go - Map에 추가된 요소의 개수를 확인하는 방법
; https://www.sysnet.pe.kr/2/0/13895




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;
}
// Why is global_u16 declared volatile?
// https://ebpf-go.dev/concepts/global-variables/#global-variables

Similar to volatile const in a prior example, volatile is used here to make compiler output more deterministic. Without it, the compiler may choose to optimize away a variable if it's never assigned to, not knowing its value is actually provided by user space. The volatile qualifier doesn't change the variable's semantics.

빌드합니다.

// 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로 나오는 것을 확인할 수 있습니다.

만약 err에 다음과 같은 메시지가 담긴다면?

rewrite constants: some constants are missing from .rodata: const_u32

실제로 .rodata 섹션에 const_u32 변수 공간이 없다는 것을 의미합니다. 이런 경우 "volatile const __u32 const_u32 = 0;" 코드가 정의돼 있는지 확인한 다음, 있다면 그 변수가 bpf 내부의 코드에서 의미있게 사용됐는지도 확인해야 합니다. 가령 다음과 같은 식으로 사용됐다면,

SEC("socket") int refresh_debug_info() {
    return 5;

    if (const_u32 == 0) {
        return 1;
    }
}

bpf2go 빌드 후에는 const_u32 변수의 사용이 (그 전에 return 5; 구문 때문에) 최적화에 의해 사라지게 되고, 결국 어떤 곳에서도 해당 변수를 사용하지 않으므로 .rodata 섹션에서도 빠지게 됩니다.




참고로, 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
}

// 만약 err 값이 "{error | sys.wrappedErrno} bad file descriptor"라고 나온다면, bpfObj를 이미 Close한 경우일 수 있습니다.

정상적으로 코드가 적용됐는지 확인할 수 있습니다. 위에서 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을 이용해도 됩니다.)




본문의 예제를,

volatile const __u32 debug_mode = 0;

SEC("socket") int refresh_debug_info() {
    if (debug_mode == 0) {
        return 1;
    }

    return 0;
}

5.2 미만의 리눅스 커널 환경에서 로드하면 이런 오류가 발생합니다.

/*
$ uname -r
5.1.15-1.el7.elrepo.x86_64
*/

err = spec.LoadAndAssign(&_bpfObj, &opts)

// field RefreshDebugInfo: program refresh_debug_info: map .rodata: map create: read- and write-only maps not supported (requires >= v5.2)

.rodata 섹션에 있는 debug_mode 변수를 처리할 수 없다고 하는데요,

$ llvm-objdump -dj .rodata ebpf_main_x86_bpfel.o

ebpf_main_x86_bpfel.o:  file format ELF64-BPF


Disassembly of section .rodata:

0000000000000000 debug_mode:
       0:       00      <unknown>
       0:       00      <unknown>
       0:       00      <unknown>
       0:       00      <unknown>

이에 대한 이슈가 cilium에 이미 등록돼 있었습니다.

.rodata: map create: read- and write-only maps not supported (requires >= v5.2) #628
; https://github.com/cilium/ebpf/issues/628

오류 메시지 자체는 ebpf go 언어 측 패키지에서 나오는 것이지만, 어쨌든 커널 측에서 지원하지 않기 때문에 어쩔 수 없다는 답글이 있습니다. (cilium 패키지에 의존하는 다른 repo에서도 이에 따른 변화가 보입니다.)

참고로, 이건 const에만 해당되는 것이 아니고 전역 변수 자체를 다룰 수가 없게 돼 있습니다. 예를 들어 코드를 다음과 같이 바꾸면,

static int env_info = 5;

SEC("socket") int refresh_debug_info() {

    env_info ++;
    return env_info;
}

env_info는 ".data" 섹션에 존재하는데,

$ llvm-objdump -dj .data ebpf_main_x86_bpfel.o

ebpf_main_x86_bpfel.o:  file format ELF64-BPF


Disassembly of section .data:

0000000000000000 env_info:
       0:       05      <unknown>
       0:       00      <unknown>
       0:       00      <unknown>
       0:       00      <unknown>

이번에도 LoadAndAssign 단계에서 이런 오류가 발생합니다.

field RefreshDebugInfo: program refresh_debug_info: load program: invalid argument: unrecognized bpf_ld_imm64 insn

결국 전역 변수를 함수 내에서 사용하는 것이 문제라는 건데요, 따라서 Kconfig도 전역 변수의 사용이기 때문에 커널 5.2 미만에서는 이런 오류가 발생합니다.

extern int LINUX_KERNEL_VERSION __kconfig;

SEC("socket") int refresh_debug_info() {
    return LINUX_KERNEL_VERSION;
}

/* __kconfig can not be used < 5.2 #1119

field RefreshDebugInfo: program refresh_debug_info: resolve .kconfig: creating map: map create: read- and write-only maps not supported (requires >= v5.2)
*/




[이 글에 대해서 여러분들과 의견을 공유하고 싶습니다. 틀리거나 미흡한 부분 또는 의문 사항이 있으시면 언제든 댓글 남겨주십시오.]

[연관 글]






[최초 등록일: ]
[최종 수정일: 3/4/2025]

Creative Commons License
이 저작물은 크리에이티브 커먼즈 코리아 저작자표시-비영리-변경금지 2.0 대한민국 라이센스에 따라 이용하실 수 있습니다.
by SeongTae Jeong, mailto:techsharer at outlook.com

비밀번호

댓글 작성자
 




1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...
NoWriterDateCnt.TitleFile(s)
13693정성태7/24/20247225개발 환경 구성: 717. Visual Studio - C# 프로젝트에서 레지스트리에 등록하지 않은 COM 개체 참조 및 사용 방법파일 다운로드1
13692정성태7/24/20248002디버깅 기술: 199. Windbg - 리눅스에서 뜬 닷넷 응용 프로그램 덤프 파일에 포함된 DLL의 Export Directory 탐색
13691정성태7/23/20247360디버깅 기술: 198. Windbg - 스레드의 Win32 Message Queue 정보 조회
13690정성태7/23/20246993오류 유형: 919. Visual C++ 리눅스 프로젝트 - error : ‘u8’ was not declared in this scope
13689정성태7/22/20248489디버깅 기술: 197. Windbg - PE 포맷의 Export Directory 탐색
13688정성태7/21/20247603닷넷: 2281. C# - Lock / Wait 상태에서도 일부 Win32 메시지 처리파일 다운로드1
13687정성태7/19/20248036닷넷: 2280. C# - PostThreadMessage로 보낸 메시지를 Windows Forms에서 수신하는 방법파일 다운로드1
13686정성태7/19/20247830오류 유형: 918. Visual Studio - ATL Simple Object 추가 시 error C2065: 'IDR_...': undeclared identifier
13685정성태7/19/20247981스크립트: 66. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법 - 두 번째 이야기
13684정성태7/19/20248145닷넷: 2279. C# - 문자열 보간식 사례 (예: 조건 연산자 사용)
13683정성태7/18/20247631오류 유형: 917. ClrMD - Linux 환경의 .NET 5 덤프 분석 시 hang 현상
13682정성태7/18/20247842닷넷: 2278. WPF - 스레드에 종속되는 DependencyObject파일 다운로드1
13681정성태7/17/20247453닷넷: 2277. C# 13 - (2) 메서드 그룹의 자연 타입 개선 (메서드 추론 개선)파일 다운로드1
13680정성태7/16/20247810닷넷: 2276. C# - Method Group, Natural Type, function_type파일 다운로드1
13679정성태7/16/20246919Linux: 76. Linux - C++ (getaddrinfo 등을 담고 있는) libnss 정적 링크
13678정성태7/15/20247045VS.NET IDE: 191. Visual Studio 2022 - .NET 5 프로젝트를 Docker Support로 실행했을 때 오류
13677정성태7/15/20247116오류 유형: 916. MSBuild - CheckEolTargetFramework (warning NETSDK1138)
13676정성태7/14/20247307Linux: 75. gdb에서 glibc의 함수에 Breakpoint 걸기
13675정성태7/13/20249086C/C++: 166. C/C++ - DLL에서 template 함수를 export하는 방법 [1]파일 다운로드1
13674정성태7/13/20247973오류 유형: 915. Unhandled Exception: Microsoft.Diagnostics.NETCore.Client.ServerNotAvailableException: Unable to connect to Process
13673정성태7/11/20248414닷넷: 2275. C# 13 - (1) 신규 이스케이프 시퀀스 '\e'파일 다운로드1
13672정성태7/10/20247138닷넷: 2274. IIS - (프로세스 종료 없는) AppDomain Recycle
13671정성태7/10/20247246오류 유형: 914. Package ca-certificates is not installed.
13669정성태7/9/20247358오류 유형: 913. C# - AOT StaticExecutable 정적 링킹 시 빌드 오류
13668정성태7/8/20247380개발 환경 구성: 716. Hyper-V - Ubuntu 22.04 Generation 2 유형의 VM 설치
13667정성태7/7/20246600닷넷: 2273. C# - 리눅스 환경에서의 Hyper-V Socket 연동 (AF_VSOCK)파일 다운로드1
1  2  3  4  5  6  7  8  9  [10]  11  12  13  14  15  ...