Microsoft MVP성태의 닷넷 이야기
Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법 [링크 복사], [링크+제목 복사],
조회: 4814
글쓴 사람
정성태 (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

비밀번호

댓글 작성자
 




... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...
NoWriterDateCnt.TitleFile(s)
13490정성태12/19/202310105개발 환경 구성: 698. Golang - GLIBC 의존을 없애는 정적 빌드 방법
13489정성태12/19/202310049개발 환경 구성: 697. GoLand에서 ldflags 지정 방법
13488정성태12/18/20239525오류 유형: 884. HTTP 500.0 - 명령행에서 실행한 ASP.NET Core 응용 프로그램을 실행하는 방법
13487정성태12/16/202310622개발 환경 구성: 696. C# - 리눅스용 AOT 빌드를 docker에서 수행 [1]
13486정성태12/15/20239731개발 환경 구성: 695. Nuget config 파일에 값 설정/삭제 방법
13485정성태12/15/20239418오류 유형: 883. dotnet build/restore - error : Root element is missing
13484정성태12/14/202310156개발 환경 구성: 694. Windows 디렉터리 경로를 WSL의 /mnt 포맷으로 구하는 방법
13483정성태12/14/202310418닷넷: 2184. C# - 하나의 resource 파일을 여러 프로그램에서 (AOT 시에도) 사용하는 방법파일 다운로드1
13482정성태12/13/202311643닷넷: 2183. C# - eFriend Expert OCX 예제를 .NET Core/5+ Console App에서 사용하는 방법 [2]파일 다운로드1
13481정성태12/13/202310530개발 환경 구성: 693. msbuild - .NET Core/5+ 프로젝트에서 resgen을 이용한 리소스 파일 생성 방법파일 다운로드1
13480정성태12/12/202312387개발 환경 구성: 692. Windows WSL 2 + Chrome 웹 브라우저 설치
13479정성태12/11/202310021개발 환경 구성: 691. WSL 2 (Ubuntu) + nginx 환경 설정
13477정성태12/8/202310664닷넷: 2182. C# - .NET 7부터 추가된 Int128, UInt128 [1]파일 다운로드1
13476정성태12/8/202310513닷넷: 2181. C# - .NET 8 JsonStringEnumConverter의 AOT를 위한 개선파일 다운로드1
13475정성태12/7/202310773닷넷: 2180. .NET 8 - 함수 포인터에 대한 Reflection 정보 조회파일 다운로드1
13474정성태12/6/202310345개발 환경 구성: 690. 닷넷 코어/5+ 버전의 ilasm/ildasm 실행 파일 구하는 방법 - 두 번째 이야기
13473정성태12/5/202310686닷넷: 2179. C# - 값 형식(Blittable)을 메모리 복사를 이용해 바이트 배열로 직렬화/역직렬화파일 다운로드1
13472정성태12/4/202310104C/C++: 164. Visual C++ - InterlockedCompareExchange128 사용 방법
13471정성태12/4/202310640Copilot - To enable GitHub Copilot, authorize this extension using GitHub's device flow
13470정성태12/2/202311264닷넷: 2178. C# - .NET 8부터 COM Interop에 대한 자동 소스 코드 생성 도입 [1]파일 다운로드1
13469정성태12/1/202311170닷넷: 2177. C# - (Interop DLL 없이) CoClass를 이용한 COM 개체 생성 방법파일 다운로드1
13468정성태12/1/202310176닷넷: 2176. C# - .NET Core/5+부터 달라진 RCW(Runtime Callable Wrapper) 대응 방식파일 다운로드1
13467정성태11/30/202310933오류 유형: 882. C# - Unhandled exception. System.Runtime.InteropServices.COMException (0x800080A5)파일 다운로드1
13466정성태11/29/202310758닷넷: 2175. C# - DllImport 메서드의 AOT 지원을 위한 LibraryImport 옵션
13465정성태11/28/202310466개발 환경 구성: 689. MSBuild - CopyToOutputDirectory가 "dotnet publish" 시에는 적용되지 않는 문제파일 다운로드1
13464정성태11/28/202310371닷넷: 2174. C# - .NET 7부터 UnmanagedCallersOnly 함수 export 기능을 AOT 빌드에 통합파일 다운로드1
... 16  17  [18]  19  20  21  22  23  24  25  26  27  28  29  30  ...