Microsoft MVP성태의 닷넷 이야기
Linux: 105. eBPF - bpf2go에서 전역 변수 설정 방법 [링크 복사], [링크+제목 복사],
조회: 4810
글쓴 사람
정성태 (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)
13388정성태7/3/202311865오류 유형: 871. 윈도우 탐색기에서 열리지 않는 zip 파일 - The Compressed (zipped) Folder '[...].zip' is invalid. [1]파일 다운로드1
13387정성태6/28/202311101오류 유형: 870. _mysql - Commands out of sync; you can't run this command now
13386정성태6/27/202311799Linux: 61. docker - 원격 제어를 위한 TCP 바인딩 추가
13385정성태6/27/202311677Linux: 60. Linux - 외부에서의 접속을 허용하기 위한 TCP 포트 여는 방법
13384정성태6/26/202311156.NET Framework: 2131. C# - Source Generator로 해결하는 enum 박싱 문제파일 다운로드1
13383정성태6/26/202310953개발 환경 구성: 683. GPU 런타임을 사용하는 Colab 노트북 설정
13382정성태6/25/202310845.NET Framework: 2130. C# - Win32 API를 이용한 윈도우 계정 정보 (예: 마지막 로그온 시간)파일 다운로드1
13381정성태6/25/202311795오류 유형: 869. Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
13380정성태6/24/202310235스크립트: 52. 파이썬 3.x에서의 동적 함수 추가
13379정성태6/23/202310695스크립트: 51. 파이썬 2.x에서의 동적 함수 추가
13378정성태6/22/202310895오류 유형: 868. docker - build 시 "CANCELED ..." 뜨는 문제
13377정성태6/22/202315933오류 유형: 867. 파이썬 mysqlclient 2.2.x 설치 시 "Specify MYSQLCLIENT_CFLAGS and MYSQLCLIENT_LDFLAGS env vars manually" 오류
13376정성태6/21/202311658.NET Framework: 2129. C# - Polly를 이용한 클라이언트 측의 요청 재시도파일 다운로드1
13375정성태6/20/202310696스크립트: 50. Transformers (신경망 언어모델 라이브러리) 강좌 - 2장 코드 실행 결과
13374정성태6/20/202310396오류 유형: 866. 파이썬 - <class 'AttributeError'> module 'flask.json' has no attribute 'JSONEncoder'
13373정성태6/19/202311789오류 유형: 865. 파이썬 - pymssql 설치 관련 오류 정리
13372정성태6/15/202310568개발 환경 구성: 682. SQL Server TLS 통신을 위해 사용되는 키 길이 확인 방법
13371정성태6/15/202311021개발 환경 구성: 681. openssl - 인증서 버전(V1 / V3)
13370정성태6/14/202311283개발 환경 구성: 680. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 - TLS 1.2 지원
13369정성태6/13/202310575개발 환경 구성: 679. PyCharm(을 비롯해 JetBrains에 속한 여타) IDE에서 내부 Window들의 탭이 없어진 경우
13368정성태6/13/202311350개발 환경 구성: 678. openssl로 생성한 인증서를 SQL Server의 암호화 인증서로 설정하는 방법
13367정성태6/10/202312136오류 유형: 864. openssl로 만든 pfx 인증서를 Windows Server 2016 이하에서 등록 시 "The password you entered is incorrect" 오류 발생
13366정성태6/10/202311264.NET Framework: 2128. C# - 윈도우 시스템에서 지원하는 암호화 목록(Cipher Suites) 나열파일 다운로드1
13365정성태6/8/202310192오류 유형: 863. MODIFY FILE encountered operating system error 112(failed to retrieve text for this error. Reason: 15105)
13364정성태6/8/202312342.NET Framework: 2127. C# - Ubuntu + Microsoft.Data.SqlClient + SQL Server 2008 R2 연결 방법 [1]
13363정성태6/7/202312007스크립트: 49. 파이썬 - "Transformers (신경망 언어모델 라이브러리) 강좌" - 1장 2절 코드 실행 결과
... 16  17  18  19  20  21  [22]  23  24  25  26  27  28  29  30  ...