From 9860624ef46c7f1aee14c006e073698bf6549da1 Mon Sep 17 00:00:00 2001 From: Takeru Hayasaka Date: Sat, 14 Sep 2024 04:36:34 +0900 Subject: [PATCH] feat: add kprobe sample and link impli --- lib/Sys/Ebpf/Asm.pm | 4 +- lib/Sys/Ebpf/Elf/Parser.pm | 2 +- lib/Sys/Ebpf/Link/Perf/Arch.pm | 40 ++++++ lib/Sys/Ebpf/Link/Perf/Kprobe.pm | 216 +++++++++++++++++++++++++++++++ lib/Sys/Ebpf/Loader.pm | 17 ++- lib/Sys/Ebpf/Map.pm | 9 +- sample/kprobe/kprobe.pl | 62 +++++---- 7 files changed, 308 insertions(+), 42 deletions(-) create mode 100644 lib/Sys/Ebpf/Link/Perf/Arch.pm create mode 100644 lib/Sys/Ebpf/Link/Perf/Kprobe.pm diff --git a/lib/Sys/Ebpf/Asm.pm b/lib/Sys/Ebpf/Asm.pm index 922fbc7..9ee697a 100644 --- a/lib/Sys/Ebpf/Asm.pm +++ b/lib/Sys/Ebpf/Asm.pm @@ -175,8 +175,8 @@ sub get_imm { $_[0]->{imm} } sub deserialize_128bit_instruction { my ($binary) = @_; - my $high = Sys::Ebpf::asm->deserialize( substr( $binary, 0, 8 ) ); - my $low = Sys::Ebpf::asm->deserialize( substr( $binary, 8, 8 ) ); + my $high = Sys::Ebpf::Asm->deserialize( substr( $binary, 0, 8 ) ); + my $low = Sys::Ebpf::Asm->deserialize( substr( $binary, 8, 8 ) ); return ( $high, $low ); } diff --git a/lib/Sys/Ebpf/Elf/Parser.pm b/lib/Sys/Ebpf/Elf/Parser.pm index b9397ed..fcd86d2 100644 --- a/lib/Sys/Ebpf/Elf/Parser.pm +++ b/lib/Sys/Ebpf/Elf/Parser.pm @@ -80,7 +80,7 @@ sub parse_elf { $elf->{e_type} = $e_type; $elf->{e_machine} = $e_machine; $elf->{e_machine_name} - = Sys::Ebpf::elf::MachineType->get_machine_name($e_machine); + = Sys::Ebpf::Elf::MachineType->get_machine_name($e_machine); $elf->{e_version} = $e_version; $elf->{e_entry} = $e_entry; $elf->{e_phoff} = $e_phoff; diff --git a/lib/Sys/Ebpf/Link/Perf/Arch.pm b/lib/Sys/Ebpf/Link/Perf/Arch.pm new file mode 100644 index 0000000..c7ed218 --- /dev/null +++ b/lib/Sys/Ebpf/Link/Perf/Arch.pm @@ -0,0 +1,40 @@ +package Sys::Ebpf::Link::Perf::Arch; + +use strict; +use warnings; +use utf8; + +use Config qw( %Config ); + +sub platform_prefix { + my $arch = $Config{archname}; + if ( $arch =~ /^i[3456]86/ ) { + return "__ia32_"; + } + elsif ( $arch =~ /^x86_64/ ) { + return "__x64_"; + } + elsif ( $arch =~ /^arm/ ) { + return "__arm_"; + } + elsif ( $arch =~ /^aarch64/ ) { + return "__arm64_"; + } + elsif ( $arch =~ /^mips/ ) { + return "__mips_"; + } + elsif ( $arch =~ /^s390/ ) { + return "__s390_"; + } + elsif ( $arch =~ /^powerpc/ ) { + return $arch =~ /64/ ? "__powerpc64_" : "__powerpc_"; + } + elsif ( $arch =~ /^riscv/ ) { + return "__riscv_"; + } + else { + return ""; + } +} + +1; diff --git a/lib/Sys/Ebpf/Link/Perf/Kprobe.pm b/lib/Sys/Ebpf/Link/Perf/Kprobe.pm new file mode 100644 index 0000000..a3fcfa5 --- /dev/null +++ b/lib/Sys/Ebpf/Link/Perf/Kprobe.pm @@ -0,0 +1,216 @@ +package Sys::Ebpf::Link::Perf::Kprobe; + +use strict; +use warnings; +use utf8; + +use POSIX qw( close fcntl open sprintf ); +use Fcntl qw( F_GETFL ); +use IO::Handle qw( print ); +use Sys::Ebpf::Link::Perf::Arch qw( platform_prefix ); +use open ':std', ':encoding(UTF-8)'; + +use constant { + PERF_EVENT_IOC_ENABLE => 0x2400, + PERF_EVENT_IOC_DISABLE => 0x2401, + PERF_EVENT_IOC_SET_BPF => 0x40042408, + PERF_TYPE_TRACEPOINT => 2, + PERF_SAMPLE_RAW => 1 << 10, + PERF_FLAG_FD_CLOEXEC => 0x00000008, + SYS_perf_event_open => 298, # x86_64 システムの場合。他のアーキテクチャでは異なる可能性があります。 + SYS_ioctl => 16, +}; + +sub attach_kprobe { + my ( $prog_fd, $kprobe_fn ) = @_; + + # kprobe 関数名の抽出 + my $func_name; + if ( $kprobe_fn =~ m{^kprobe/(.+)$} ) { + $func_name = $1; + } + else { + die "無効な kprobe 関数名です: $kprobe_fn"; + } + + my $prefix = Sys::Ebpf::Link::Perf::Arch::platform_prefix(); + $func_name = $prefix . $func_name unless $func_name =~ /^$prefix/; + + # ユニークなイベント名を生成 + my $current_pid = $$; + my $event_name = "p_${func_name}_${current_pid}"; + + # kprobe_events ファイルのパスを動的に取得 + my $kprobe_events; + if ( -e '/sys/kernel/debug/tracing/kprobe_events' ) { + $kprobe_events = '/sys/kernel/debug/tracing/kprobe_events'; + } + elsif ( -e '/sys/kernel/tracing/kprobe_events' ) { + $kprobe_events = '/sys/kernel/tracing/kprobe_events'; + } + else { + die "kprobe_events ファイルが見つかりません"; + } + + print "kprobe_events: $kprobe_events\n"; + print "p:$event_name $func_name\n"; + + # kprobe イベントの作成 + open my $fh, '>>', $kprobe_events or die "kprobe_events を開けません: $!"; + $fh->autoflush(1); + print $fh "p:$event_name $func_name\n" + or die "kprobe_events への書き込みに失敗しました: $!"; + close $fh or warn "kprobe_events のクローズに失敗しました: $!"; + + # イベント ID の取得 + my $id_file = $kprobe_events; + $id_file =~ s/kprobe_events/events\/kprobes\/$event_name\/id/; + unless ( -e $id_file ) { + die "イベント ID ファイルが存在しません。kprobe イベントの作成に失敗した可能性があります。"; + } + open my $id_fh, '<', $id_file or die "イベント ID ファイルを開けません: $!"; + my $id = <$id_fh>; + chomp $id; + close $id_fh; + + my $pack_str = "L" . # type, __u32 + "L" . #size, __u32 + "Q" . #config, __u64 + "Q" . #sample_period, __u64 + "Q" . #sample_type, __u64 + "Q" . #read_format, __u64 + "Q" . #ビットフィールド (disabled, inherit, etc.), __u64 + "L" . #wakeup_events, __u32 + "L" . #bp_type, __u32 + "Q" . #config1 (bp_addr / kprobe_func / uprobe_path), __u64 + "Q" . #config2 (bp_len / kprobe_addr / probe_offset), __u64 + "Q" . #branch_sample_type, __u64 + "Q" . #sample_regs_user, __u64 + "L" . #sample_stack_user, __u32 + "L" . #clockid, __s32 + "Q" . #sample_regs_intr, __u64 + "L" . #aux_watermark. __u32 + "S" . #sample_max_stack, __u16 + "S" . #__reserved_2, __u16 + "L" . #aux_sample_size, __u32 + "L" . #__reserved_3, __u32 + "Q" . #sig_data, __u64 + "Q"; #config3, __u64 + my $attr_size = 0; + $attr_size += { C => 1, S => 2, L => 4, Q => 8 }->{$_} + for split //, $pack_str; + print "attr_size: $attr_size\n"; + my $attr = pack( + $pack_str, + PERF_TYPE_TRACEPOINT, # type, __u32 + $attr_size, # size (構造体全体のサイズ), __u32 + $id, # config (kprobe_id), __u64 + 1, # sample_period, __u64 + PERF_SAMPLE_RAW, # sample_type, __u64 + 0, # read_format, __u64 + 0, # ビットフィールド (disabled, inherit, etc.), __u64 + 1, # wakeup_events, __u32 + 0, # bp_type, __u32 + 0, # config1 (bp_addr / kprobe_func / uprobe_path), __u64 + 0, # config2 (bp_len / kprobe_addr / probe_offset), __u64 + 0, # branch_sample_type, __u64 + 0, # sample_regs_user, __u64 + 0, # sample_stack_user, __u32 + 0, # clockid, __s32 + 0, # sample_regs_intr, __u64 + 0, # aux_watermark. __u32 + 0, # sample_max_stack, __u16 + 0, # __reserved_2, __u16 + 0, # aux_sample_size, __u32 + 0, # __reserved_3, __u32 + 0, # sig_data, __u64 + 0, # config3, __u64 + ); + + # perf_event_open システムコールの呼び出し + my $target_pid = -1; # すべてのプロセスを監視 + my $cpu = 0; # すべてのCPUで監視 + my $group_fd = -1; + my $flags = PERF_FLAG_FD_CLOEXEC; + + my $perf_fd + = syscall( SYS_perf_event_open, $attr, $target_pid, $cpu, $group_fd, + $flags ); + if ( $perf_fd < 0 ) { + my $errno = $! + 0; + my $error_msg = $!; + die sprintf( "perf イベントのオープンに失敗しました: %s (errno: %d)\n", + $error_msg, $errno ); + } + + print "perf_event_open successful. File descriptor: $perf_fd\n"; + + # BPF プログラムを perf イベントにアタッチ + $! = 0; # reset errno + my $res + = syscall( &SYS_ioctl, $perf_fd, PERF_EVENT_IOC_SET_BPF, $prog_fd ); + my $errno = $! + 0; + print "ioctl res: $res\n"; + if ( !defined $res || $res < 0 ) { + my $error_msg = $!; + die sprintf( "BPF プログラムのアタッチに失敗しました: %s (errno: %d)\n", + $error_msg, $errno ); + } + + print "BPF program attached successfully\n"; + + # perf イベントを有効化 + $! = 0; # reset errno + # $res = ioctl($perf_fh, PERF_EVENT_IOC_ENABLE, 0)|| -1; + $res = syscall( &SYS_ioctl, $perf_fd, PERF_EVENT_IOC_ENABLE, 0 ); + $errno = $! + 0; + if ( !defined $res || $res < 0 ) { + my $error_msg = $!; + die sprintf( "perf イベントの有効化に失敗しました: %s (errno: %d)\n", + $error_msg, $errno ); + } + + print "perf event enabled successfully\n"; + + # デタッチ時に必要な情報を返す + return { + perf_fd => $perf_fd, + event_name => $event_name, + kprobe_fn => $kprobe_fn, + kprobe_events => $kprobe_events, + }; +} + +sub check_prog_fd { + my ($prog_fd) = @_; + return 0 if $prog_fd < 0; + + my $flags = fcntl( $prog_fd, F_GETFL, 0 ); + return 0 if !defined $flags || $flags < 0; + + return 1; +} + +sub detach_kprobe { + my ($kprobe_info) = @_; + + my $perf_fd = $kprobe_info->{perf_fd}; + my $event_name = $kprobe_info->{event_name}; + my $kprobe_events = $kprobe_info->{kprobe_events}; + + # perf イベントを無効化 + my $res = syscall( SYS_ioctl, $perf_fd, PERF_EVENT_IOC_DISABLE, 0 ); + if ( $res != 0 ) { + warn "perf イベントの無効化に失敗しました: $!"; + } + + # perf イベントのファイルディスクリプタをクローズ + close($perf_fd); + + # kprobe イベントの削除 + open my $fh, '>>', $kprobe_events or warn "kprobe_events を開けません: $!"; + $fh->autoflush(1); + print $fh "-:$event_name\n" or warn "kprobe_events への書き込みに失敗しました: $!"; + close $fh or warn "kprobe_events のクローズに失敗しました: $!"; +} +1; diff --git a/lib/Sys/Ebpf/Loader.pm b/lib/Sys/Ebpf/Loader.pm index d767577..7292df0 100644 --- a/lib/Sys/Ebpf/Loader.pm +++ b/lib/Sys/Ebpf/Loader.pm @@ -219,12 +219,12 @@ sub load_bpf_program { # prob_section: プログラムセクション # reloc_sections: リロケーションセクション # elf: ELFデータ -# map_data: マップデータのリファレンス +# map_hash_ref: マップデータのリファレンス # r_offsetを使って、修正すべき命令(インストラクション)をkprobe/sys_execveセクション内から特定します。 # r_infoからシンボルインデックスを取得し、そのシンボルのアドレスをシンボルテーブル(.symtab)から取得します。 # 修正すべきインストラクションに、シンボルのアドレスを適用して、正しいマップへの参照に書き換えます。 sub apply_map_relocations { - my ( $self, $prob_section, $reloc_sections, $elf, $map_data ) = @_; + my ( $self, $prob_section, $reloc_sections, $elf, $map_hash_ref ) = @_; my $symbols_section = $elf->{symbols}; for my $reloc_section (@$reloc_sections) { my $r_info = $reloc_section->{r_info}; @@ -249,14 +249,13 @@ sub apply_map_relocations { # `$map_data` の中のタプルを確認して、期待してるマップ名が存在するかチェック(fdを取得) my $map_fd = undef; - for my $tuple (@$map_data) { - my ( $map_name, $map ) = @$tuple; + for my $map_name ( keys %$map_hash_ref ) { + my $map = $map_hash_ref->{$map_name}; if ( $sym_name eq $map_name ) { $map_fd = $map->{map_fd}; last; # マップが見つかったらループを抜ける } } - if ( defined $map_fd ) { # # 指定されたオフセット位置にある `lddw` 命令(16バイト)を取得 @@ -308,7 +307,7 @@ sub load_bpf { my $bpfelf = $self->{bpfelf}; my $maps = $self->extract_bpf_map_attributes('maps'); - my @map_collection; + my %map_collection; # map_attr_refの各キー(マップ名)に対して処理を実行 for my $map (@$maps) { @@ -318,7 +317,7 @@ sub load_bpf { if ( $map_fd < 0 ) { die "Failed to load BPF map: $map_name (FD: $map_fd})\n"; } - push @map_collection, [ $map_name, $map_instance ]; + $map_collection{$map_name} = $map_instance; } # リロケーションを適用 @@ -328,14 +327,14 @@ sub load_bpf { = find_symbol_table_from_name( $bpfelf->{sections}, $section_name ); if ( defined $reloc_section ) { $self->apply_map_relocations( $prob_section, $reloc_section, $bpfelf, - \@map_collection ); + \%map_collection ); } # todo: bpfprobが複数あるケースにも対応する # BPF プログラムをロード my $prog_fd = $self->load_bpf_program_from_elf($section_name); - return ( \@map_collection, $prog_fd ); + return ( \%map_collection, $prog_fd ); } 1; diff --git a/lib/Sys/Ebpf/Map.pm b/lib/Sys/Ebpf/Map.pm index 62025bc..92bc326 100644 --- a/lib/Sys/Ebpf/Map.pm +++ b/lib/Sys/Ebpf/Map.pm @@ -14,9 +14,9 @@ use Sys::Ebpf::Constants::BpfCmd qw( BPF_MAP_UPDATE_ELEM BPF_OBJ_PIN ); -use Sys::Ebpf::Constants::BpfMapType qw(:all); -use Sys::Ebpf::Constants::BpfMapUpdateFlags qw(:all); -use Sys::Ebpf::Constants::BpfMapCreateFlags qw(:all); +use Sys::Ebpf::Constants::BpfMapType qw( BPF_MAP_TYPE_UNSPEC ); +use Sys::Ebpf::Constants::BpfMapUpdateFlags qw( BPF_ANY ); +use Sys::Ebpf::Constants::BpfMapCreateFlags (); use Sys::Ebpf::Syscall; sub new { @@ -203,6 +203,9 @@ sub update { sub lookup { my ( $self, $key ) = @_; + if ( !defined $key ) { + die "Key must be defined to use lookup method\n"; + } if ( !defined $self->{key_schema} || !defined $self->{value_schema} ) { die "Key and value schema must be defined to use update method\n"; } diff --git a/sample/kprobe/kprobe.pl b/sample/kprobe/kprobe.pl index 7f399af..6f2a706 100644 --- a/sample/kprobe/kprobe.pl +++ b/sample/kprobe/kprobe.pl @@ -4,40 +4,48 @@ use warnings; use utf8; use lib '../../lib'; # lib ディレクトリへの相対パスを追加 -use Sys::Ebpf::loader; -use Data::Dumper (); - -use Sys::Ebpf::Constants::bpf_map_type (); +use Sys::Ebpf::Loader; +use Sys::Ebpf::Link::Perf::Kprobe; +use Data::Dumper qw( Dumper ); my $file = "kprobe.o"; -my $loader = Sys::Ebpf::loader->new($file); +my $loader = Sys::Ebpf::Loader->new($file); my $data = $loader->load_elf(); my ( $map_data, $prog_fd ) = $loader->load_bpf("kprobe/sys_execve"); - -# # Kprobeをアタッチ -# my $kprobe_info = $loader->attach_kprobe($prog_fd, $fn); - -# print "Waiting for events..\n"; - -# # 1秒ごとにマップの値を読み取り、表示 -# while (1) { -# my $value = $map->lookup(pack('L', 0)); -# if (defined $value) { -# my $count = unpack('Q', $value); -# printf "%s called %d times\n", $fn, $count; -# } else { -# warn "Failed to read map value\n"; -# } -# sleep(1); -# } +print "prog_fd: $prog_fd\n"; +print Dumper($map_data); +my $map_kprobe_map = $map_data->{kprobe_map}; +$map_kprobe_map->{key_schema} = [ [ 'kprobe_map_key', 'uint32' ], ]; +$map_kprobe_map->{value_schema} = [ [ 'kprobe_map_value', 'uint64' ], ]; +my $kprobe_fn = "kprobe/sys_execve"; + +# Kprobeをアタッチ +my $kprobe_info + = Sys::Ebpf::Link::Perf::Kprobe::attach_kprobe( $prog_fd, $kprobe_fn ); + +print "Waiting for events..\n"; + +# 1秒ごとにマップの値を読み取り、表示 +while (1) { + my $key = { kprobe_map_key => 0 }; + my $value = $map_kprobe_map->lookup($key); + if ( defined $value ) { + print Dumper($value); + printf "%s called %d times\n", $kprobe_fn, $value->{kprobe_map_value}; + } + else { + warn "Failed to read map value\n"; + } + sleep(1); +} # # クリーンアップ(この部分は実際には実行されませんが、適切な終了処理のために必要です) -# END { -# if ($loader && $kprobe_info) { -# $loader->detach_kprobe($kprobe_info); -# } -# } +END { + if ($kprobe_info) { + Sys::Ebpf::Link::Perf::Kprobe::detach_kprobe($kprobe_info); + } +} # いろいろな出力方法があるっぽい # print Dumper($data);