
eBPF 프로그램을 위한 Rust 가상 머신 및 JIT 컴파일러
eBPF를 위한 Rust (사용자 공간) 가상 머신
이 crate는 eBPF 프로그램 실행을 위한 가상 머신을 포함합니다. BPF는 _Berkeley Packet Filter_의 약자로, 원래 BSD 시스템을 위해 개발된 어셈블리와 유사한 언어로, tcpdump와 같은 도구를 사용하여 커널에서 패킷을 필터링함으로써 사용자 공간으로의 불필요한 복사를 피하기 위한 것입니다. 이는 Linux로 이식되었고, 더 빠르고 더 많은 기능을 갖춘 버전인 eBPF(extended BPF)로 발전했습니다. BPF 프로그램은 원래 커널에서 실행되도록 설계되었지만, 이 crate의 가상 머신은 사용자 공간 애플리케이션에서 실행할 수 있게 해줍니다. 여기에는 인터프리터, eBPF 프로그램을 위한 x86_64 JIT 컴파일러, 그리고 디스어셈블러가 포함되어 있습니다.
이는 거의 동일한 기능을 하지만 C로 작성된 Rich Lane의 uBPF 소프트웨어를 기반으로 합니다.
이 crate는 Linux, MacOS X, Windows에서 컴파일 및 실행되어야 하지만, JIT 컴파일러는 현재 Windows에서 작동하지 않습니다.
이 crate는 crates.io에서 사용할 수 있으므로,
Cargo.toml 파일에 의존성으로 추가하면 바로 작동할 것입니다:
[dependencies]
rbpf = "0.4.1"
개발 버전은 이 GitHub 저장소에서도 사용할 수 있습니다. 이는
Cargo.toml 안에 다음을 넣는 것만큼 간단해야 합니다:
[dependencies]
rbpf = { git = "https://github.com/qmonnet/rbpf" }
물론, 원한다면 로컬에 클론하여 크레이트를 수정한 뒤, Cargo.toml에 로컬 버전의 경로를 지정할 수도 있습니다:
[dependencies]
rbpf = { path = "path/to/rbpf" }
그런 다음 소스 코드에서 해당 crate를 사용하겠다고 명시하세요:
extern crate rbpf;
API는 소스 코드 내에 상당히 잘 문서화되어 있습니다. 또한 여기에서 온라인 버전의 문서에 접근할 수 있으며, 이는 crates.io 버전에서 자동 생성된 것입니다(메인 브랜치와 최신 상태가 아닐 수 있음). 예제와 단위 테스트도 도움이 될 것입니다. 다음은 이 크레이트를 사용하는 방법에 대한 요약입니다.
rbpf로 eBPF 프로그램을 실행하기 위해 따라야 할 단계는 다음과 같습니다:
eBPF는 처음에 패킷을 필터링하도록 설계되었습니다(현재는 Linux 커널에 kprobes와 같은 다른 훅이 있지만, 이는 rbpf에서 다루지 않습니다). 결과적으로 프로그램의 대부분의 로드 및 저장 명령은 패킷 데이터를 나타내는 메모리 영역에서 수행됩니다. 그러나 Linux 커널에서 eBPF 프로그램은 이 데이터 영역에 즉시 접근하지 않습니다: 처음에는 C struct sk_buff에 접근할 수 있으며, 이는 패킷에 대한 메타데이터—패킷 데이터 영역의 시작과 끝의 메모리 주소를 포함—를 담고 있는 버퍼입니다. 따라서 프로그램은 먼저 sk_buff에서 해당 포인터를 로드한 다음 패킷 데이터에 접근할 수 있습니다.
이 동작은 rbpf로 복제할 수 있지만 필수는 아닙니다. 이러한 이유로 다양한 종류의 가상 머신을 나타내는 여러 구조체가 있습니다:
struct EbpfVmMbuffer는 커널을 모방합니다. 프로그램이 실행될 때, 첫 번째 eBPF 레지스터에 제공되는 주소는 사용자가 제공한 메타데이터 버퍼의 주소가 되며, 이 버퍼는 패킷 데이터 메모리 영역의 시작과 끝에 대한 포인터를 포함할 것으로 예상됩니다.
struct EbpfVmFixedMbuff는 한 가지 목적을 가지고 있습니다: 커널과 호환되도록 생성된 프로그램의 실행을 가능하게 하면서, 사용자가 메타데이터 버퍼를 수동으로 처리하는 수고를 덜어줍니다. 실제로 이 구조체는 프로그램에 전달되는 정적 내부 버퍼를 가지고 있습니다. 사용자는 eBPF 프로그램이 버퍼에서 패킷 데이터의 시작과 끝을 찾을 것으로 예상하는 오프셋 값을 지정해야 합니다. 프로그램을 실행하는 함수를 호출할 때(JIT 여부와 관계없이), 이 구조체는 프로그램이 호출되는 패킷 데이터의 시작과 끝에 대해 지정된 오프셋에서 이 정적 버퍼의 주소를 자동으로 업데이트합니다.
struct EbpfVmRaw는 패킷 데이터에서 직접 실행하려는 프로그램을 위한 것입니다. 메타데이터 버퍼가 관여하지 않으며, eBPF 프로그램은 첫 번째 레지스터에서 패킷 데이터의 주소를 직접 받습니다. 이것이 uBPF의 동작입니다.
struct EbpfVmNoData는 어떤 데이터도 받지 않습니다. eBPF 프로그램은 어떤 인수도 받지 않으며 반환 값은 결정적입니다. 이에 대한 유효한 사용 사례가 있을지는 확실하지 않지만, 적어도 단위 테스트에 매우 유용합니다.
이 모든 구조체는 동일한 공용 함수를 구현합니다:
// called with EbpfVmMbuff:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmMbuff<'a>, Error>
// called with EbpfVmFixedMbuff:: prefix
pub fn new(prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<EbpfVmFixedMbuff<'a>, Error>
// called with EbpfVmRaw:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmRaw<'a>, Error>
// called with EbpfVmNoData:: prefix
pub fn new(prog: &'a [u8]) -> Result<EbpfVmNoData<'a>, Error>
이는 VM의 새 인스턴스를 생성하는 데 사용된다. 반환 타입은 함수가 호출된 구조체에 따라 달라진다. 예를 들어,
rbpf::EbpfVmRaw::new(Some(my_program))는 struct rbpf::EbpfVmRaw의 인스턴스(Result로 감싸진)를 반환한다. 프로그램이 로드되면,
매우 단순한 검증기(Linux 커널용 검증기와는 전혀 다른)로 검사된다. 사용자는 이를
사용자 정의 검증기로 대체할 수도 있다.
struct EbpfVmFixedMbuff의 경우, 생성자에 두 개의 추가 인자가 전달되어야 한다:
data_offset과 data_end_offset이다. 이들은 프로그램이 실행될 때마다 패킷 데이터의
메모리 영역 시작과 끝을 가리키는 포인터가 내부 메타데이터 버퍼에 저장될
오프셋(바이트 번호)이다. 다른 구조체들은 이 메커니즘을 사용하지 않으며
해당 오프셋이 필요하지 않다.
// for struct EbpfVmMbuff, struct EbpfVmRaw and struct EbpfVmRawData
pub fn set_program(&mut self, prog: &'a [u8]) -> Result<(), Error>
// for struct EbpfVmFixedMbuff
pub fn set_program(&mut self, prog: &'a [u8],
data_offset: usize,
data_end_offset: usize) -> Result<(), Error>
예를 들어 my_vm.set_program(my_program);를 사용하여 VM 인스턴스 생성 후 로드된 프로그램을 변경할 수 있습니다. 이 프로그램은 VM에 연결된 검증기로 검사됩니다. VM의 검증 함수는 언제든지 변경할 수 있습니다.
pub type Verifier = fn(prog: &[u8]) -> Result<(), Error>;
pub fn set_verifier(&mut self,
verifier: Verifier) -> Result<(), Error>
프로그램이 이미 VM에 로드된 경우, 새 검증기를 설정하면 로드된 프로그램에서 즉시 실행된다는 점에 유의하십시오. 그러나 프로그램이 로드되지 않은 경우(VM을 생성할 때 new() 메서드에 None이 전달된 경우)에는 검증기가 실행되지 않습니다.
pub type Helper = fn (u64, u64, u64, u64, u64) -> u64;
pub fn register_helper(&mut self,
key: u32,
function: Helper) -> Result<(), Error>
이 함수는 헬퍼 함수를 등록하는 데 사용됩니다. VM은 레지스터를 해시맵에 저장하므로, 키는 원하는 어떤 u32 값이든 될 수 있습니다. 이는 Linux 커널과 호환되어야 하므로 특정 헬퍼 번호를 사용해야 하는 프로그램에 유용할 수 있습니다.
pub fn register_allowed_memory(&mut self, addrs_range: Range<u64>) -> ()
이 함수는 eBPF 프로그램이 로드하고 저장할 수 있는 메모리 주소 목록을 추가합니다. 이 함수를 여러 번 호출하면 주소가 내부 HashSet에 추가됩니다. 현재 rbpf는 인터프리터를 사용할 때만 메모리 접근을 검증합니다. 이 함수는 eBPF 맵에 저장된 객체에 대한 포인터를 반환하는 커널 헬퍼를 사용할 때 유용합니다.
// for struct EbpfVmMbuff
pub fn execute_program(&self,
mem: &'a mut [u8],
mbuff: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmFixedMbuff and struct EbpfVmRaw
pub fn execute_program(&self,
mem: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmNoData
pub fn execute_program(&self) -> Result<(u64), Error>
로드된 프로그램을 해석합니다. 이 함수는 사용된 VM의 종류에 따라 패킷 데이터와 메타데이터 버퍼에 대한 참조를 받거나, 패킷 데이터에 대한 참조만 받거나, 아무것도 받지 않습니다. 반환되는 값은 eBPF 프로그램의 결과입니다.
pub fn jit_compile(&mut self) -> Result<(), Error>
로드된 프로그램을 x86_64 아키텍처용으로 JIT 컴파일합니다. 프로그램이 헬퍼 함수를 사용해야 하는 경우, 이 함수가 호출되기 전에 VM에 등록되어야 합니다. 생성된 어셈블리 함수는 내부적으로 VM에 저장됩니다.
// for struct EbpfVmMbuff
pub unsafe fn execute_program_jit(&self, mem: &'a mut [u8],
mbuff: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmFixedMbuff and struct EbpfVmRaw
pub unsafe fn execute_program_jit(&self, mem: &'a mut [u8]) -> Result<(u64), Error>
// for struct EbpfVmNoData
pub unsafe fn execute_program_jit(&self) -> Result<(u64), Error>
JIT 컴파일된 프로그램을 호출합니다. 제공할 인수는 execute_program()과 동일하며, 이는 사용되는 VM의 종류에 따라 달라집니다. JIT 컴파일된 프로그램의 결과는 인터프리터와 동일해야 하지만, 더 빠르게 실행되어야 합니다. 프로그램 실행 중 오류가 발생하면 JIT 컴파일된 버전은 인터프리터만큼 잘 처리하지 못하며 프로그램이 충돌할 수 있다는 점에 유의하십시오. 이러한 이유로 해당 함수들은 unsafe로 표시되어 있습니다.
이는 단위 테스트 test_vm_add에서 가져온 것입니다.
extern crate rbpf;
fn main() {