Microsoft MVP성태의 닷넷 이야기
Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법 [링크 복사], [링크+제목 복사],
조회: 4797
글쓴 사람
정성태 (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)
13843정성태12/13/20244372오류 유형: 938. Docker container 내에서 빌드 시 error MSB3021: Unable to copy file "..." to "...". Access to the path '...' is denied.
13842정성태12/12/20244516디버깅 기술: 205. Windbg - KPCR, KPRCB
13841정성태12/11/20244850오류 유형: 937. error MSB4044: The "ValidateValidArchitecture" task was not given a value for the required parameter "RemoteTarget"
13840정성태12/11/20244424오류 유형: 936. msbuild - Your project file doesn't list 'win' as a "RuntimeIdentifier"
13839정성태12/11/20244842오류 유형: 936. msbuild - error CS1617: Invalid option '12.0' for /langversion. Use '/langversion:?' to list supported values.
13838정성태12/4/20244589오류 유형: 935. Windbg - Breakpoint 0's offset expression evaluation failed.
13837정성태12/3/20245039디버깅 기술: 204. Windbg - 윈도우 핸들 테이블 (3) - Windows 10 이상인 경우
13836정성태12/3/20244610디버깅 기술: 203. Windbg - x64 가상 주소를 물리 주소로 변환 (페이지 크기가 2MB인 경우)
13835정성태12/2/20245045오류 유형: 934. Azure - rm: cannot remove '...': Directory not empty
13834정성태11/29/20245270Windows: 275. C# - CUI 애플리케이션과 Console 윈도우 (Windows 10 미만의 Classic Console 모드인 경우) [1]파일 다운로드1
13833정성태11/29/20244958개발 환경 구성: 737. Azure Web App에서 Scale-out으로 늘어난 리눅스 인스턴스에 SSH 접속하는 방법
13832정성태11/27/20244900Windows: 274. Windows 7부터 도입한 conhost.exe
13831정성태11/27/20244372Linux: 111. eBPF - BPF_MAP_TYPE_PERF_EVENT_ARRAY, BPF_MAP_TYPE_RINGBUF에 대한 다양한 용어들
13830정성태11/25/20245173개발 환경 구성: 736. 파이썬 웹 앱을 Azure App Service에 배포하기
13829정성태11/25/20245134스크립트: 67. 파이썬 - Windows 버전에서 함께 설치되는 py.exe
13828정성태11/25/20244430개발 환경 구성: 735. Azure - 압축 파일을 이용한 web app 배포 시 디렉터리 구분이 안 되는 문제파일 다운로드1
13827정성태11/25/20245079Windows: 273. Windows 환경의 파일 압축 방법 (tar, Compress-Archive)
13826정성태11/21/20245311닷넷: 2313. C# - (비밀번호 등의) Console로부터 입력받을 때 문자열 출력 숨기기(echo 끄기)파일 다운로드1
13825정성태11/21/20245648Linux: 110. eBPF / bpf2go - BPF_RINGBUF_OUTPUT / BPF_MAP_TYPE_RINGBUF 사용법
13824정성태11/20/20244742Linux: 109. eBPF / bpf2go - BPF_PERF_OUTPUT / BPF_MAP_TYPE_PERF_EVENT_ARRAY 사용법
13823정성태11/20/20245273개발 환경 구성: 734. Ubuntu에 docker, kubernetes (k3s) 설치
13822정성태11/20/20245138개발 환경 구성: 733. Windbg - VirtualBox VM의 커널 디버거 연결 시 COM 포트가 없는 경우
13821정성태11/18/20245066Linux: 108. Linux와 Windows의 프로세스/스레드 ID 관리 방식
13820정성태11/18/20245232VS.NET IDE: 195. Visual C++ - C# 프로젝트처럼 CopyToOutputDirectory 항목을 추가하는 방법
13819정성태11/15/20244477Linux: 107. eBPF - libbpf CO-RE의 CONFIG_DEBUG_INFO_BTF 빌드 여부에 대한 의존성
13818정성태11/15/20245250Windows: 272. Windows 11 24H2 - sudo 추가
1  2  3  [4]  5  6  7  8  9  10  11  12  13  14  15  ...