Perhatian: berisi pemrograman sistem. Ya, pada dasarnya, itu tidak mengandung apa pun.
Mari kita bayangkan bahwa Anda diberi tugas untuk menulis game fantasi fantasi. Nah, ada tentang para elf. Dan tentang realitas virtual. Sejak kecil, Anda bermimpi menulis sesuatu seperti itu dan, tanpa ragu, setuju. Segera Anda menyadari bahwa Anda tahu sebagian besar dunia peri dari lelucon dari bashorgh tua dan sumber berbeda lainnya. Ups, masalah. Nah, di mana milik kami tidak hilang ... Diajarkan oleh pengalaman pemrograman yang kaya, Anda pergi ke Google, masukkan "spesifikasi Elf" dan ikuti tautannya. Oh! Yang ini mengarah ke semacam PDF ... jadi apa yang kita miliki di sini ... semacam Elf32_Sword
- pedang elven - sepertinya yang Anda butuhkan. 32 rupanya level karakter, dan dua merangkak di kolom berikut mungkin rusak. Persis apa yang Anda butuhkan, dan selain bagaimana sistematis! ..
Seperti yang dinyatakan dalam satu tugas pemrograman Olimpiade, setelah beberapa paragraf teks terperinci tentang topik Jepang, samurai dan geisha: "Seperti yang sudah Anda pahami, tugas itu tidak akan sama sekali tentang hal itu." Oh ya, kontes itu, tentu saja, untuk sementara waktu. Secara umum, saya menyatakan lima menit keuletan ditutup.
Hari ini saya akan mencoba berbicara tentang mem-parsing file dalam format ELF 64-bit. Pada prinsipnya, apa yang mereka tidak simpan di dalamnya adalah program asli, perpustakaan statis, perpustakaan dinamis, setiap implementasi spesifik, seperti crashdumps ... Ini digunakan, misalnya, di Linux dan banyak sistem mirip Unix lainnya, ya, kata mereka, bahkan pada ponsel dukungannya secara aktif dimasukkan dalam firmware yang ditambal sebelumnya. Tampaknya mendukung format untuk menyimpan program dari sistem operasi yang serius seharusnya sulit. Jadi saya pikir. Ya, mungkin memang begitu. Tetapi kami akan mendukung use case yang sangat spesifik: memuat bytecode eBPF dari file .o
. Kenapa begitu Hanya untuk percobaan lebih lanjut, saya akan memerlukan beberapa bytecode lintas-platform yang serius (yaitu, tidak setinggi lutut ), yang dapat diperoleh dari C daripada ditulis secara manual, sehingga eBPF sederhana dan ada backend LLVM untuk itu. Dan saya hanya perlu menguraikan ELF sebagai wadah di mana bytecode ini dimasukkan oleh kompiler.
Untuk berjaga-jaga, saya akan mengklarifikasi: artikel ini adalah program eksplorasi dan tidak mengklaim sebagai panduan lengkap. Tujuan utamanya adalah membuat bootloader yang memungkinkan Anda membaca program C yang dikompilasi dalam eBPF menggunakan Dentang - yang saya miliki - dalam volume yang cukup untuk melanjutkan percobaan.
Berita utama
Mulai dari titik nol di ELF terletak tajuk. Ini berisi huruf E, L, F, yang dapat dilihat jika Anda mencoba membukanya dengan editor teks, dan beberapa variabel global. Sebenarnya, header adalah satu-satunya struktur dalam file yang terletak pada offset tetap, dan berisi informasi untuk menemukan sisa struktur. (Selanjutnya, saya dipandu oleh dokumentasi untuk format 32-bit dan elf.h
, yang tahu tentang 64-bit. Jadi, jika Anda melihat kesalahan, silakan memperbaikinya)
Hal pertama yang memenuhi kita dalam file adalah bidang unsigned char e_ident[16]
. Ingat artikel-artikel menyenangkan ini dalam seri "semua pernyataan berikut ini salah"? Ini hampir sama: ELF dapat berisi kode 32 atau 64-bit, Little atau Big Endian, dan bahkan selusin arsitektur prosesor. Anda akan membacanya sebagai Elf64 di bawah Little endian - well, semoga sukses ... Array byte ini adalah semacam tanda tangan dari apa yang ada di dalamnya dan bagaimana menguraikannya.
Dengan empat byte pertama, semuanya sederhana - yaitu [0x7f, 'E', 'L', 'F']
. Jika mereka tidak cocok, maka ada alasan untuk percaya bahwa mereka adalah sejenis lebah yang salah. Byte berikutnya berisi kelas karakter File: ELFCLASS32
atau ELFCLASS64
- kedalaman bit. Untuk mempermudah, kami hanya akan bekerja dengan file 64-bit (adakah eBPF 32-bit?). Jika kelas ternyata ELFCLASS32
, kita cukup keluar dengan kesalahan: semua sama, struktur akan "mengambang", dan pemeriksaan kewarasan tidak ada salahnya untuk melakukannya. Byte terakhir yang menarik bagi kami dalam struktur ini menunjukkan endianness file - kami hanya akan bekerja dengan urutan byte asli untuk prosesor kami.
Untuk berjaga-jaga, saya akan mengklarifikasi: ketika bekerja dengan format ELF di C, Anda tidak boleh mengurangi setiap int dengan offset yang dihitung dengan cerdik - elf.h
berisi struktur yang diperlukan, dan nomor byte genap di e_ident
: EI_MAG0
, EI_MAG1
, EI_MAG2
, EI_MAG3
, EI_CLASS
, Anda hanya perlu membawa EI_DATA
... pointer ke data yang dibaca dari atau dipetakan ke dalam memori dari file ke pointer ke struktur dan membaca.
Selain e_ident
tajuk berisi bidang lain, beberapa hanya akan kami periksa, dan beberapa akan digunakan untuk analisis lebih lanjut, tetapi nanti. Yaitu, kami memeriksa bahwa e_machine == EM_BPF
(yaitu, "di bawah arsitektur prosesor eBPF"), e_type == ET_REL
, e_shoff != 0
. Pemeriksaan terakhir memiliki arti sebagai berikut: file dapat berisi informasi untuk menautkan (tabel bagian dan bagian), untuk meluncurkan (tabel program dan segmen), atau keduanya. Dengan dua pemeriksaan terakhir, kami memverifikasi bahwa informasi yang kami butuhkan (seolah-olah untuk ditautkan) ada dalam file. Periksa juga apakah formatnya adalah EV_CURRENT
.
Segera melakukan reservasi, saya tidak akan memeriksa validitas file, dengan asumsi bahwa jika kita memuatnya ke dalam proses kita, maka kita mempercayainya. Dalam kode kernel atau program lain yang bekerja dengan file yang tidak terpercaya, secara alami tidak mungkin melakukan hal ini .
Tabel bagian
Seperti yang saya katakan, kami tertarik pada tampilan tautan dari file, yaitu, tabel bagian dan bagian itu sendiri. Informasi tentang tempat mencari tabel bagian ada di header. Ukurannya juga ditunjukkan di sana, serta ukuran satu elemen - itu bisa lebih besar dari sizeof(Elf64_Shdr)
(karena mempengaruhi nomor versi format, jujur saya tidak tahu). Beberapa nomor bagian utama dicadangkan, dan sebenarnya tidak ada dalam tabel. Merujuk mereka memiliki arti khusus. Kami tampaknya hanya tertarik pada SHN_UNDEF
(nol juga disediakan - bagian yang hilang; omong-omong, seperti yang Anda tahu, judulnya masih ada di tabel) SHN_ABS
. Simbol "didefinisikan di bagian SHN_UNDEF
" sebenarnya tidak terdefinisi, dan di SHN_ABS
sebenarnya memiliki nilai absolut dan tidak dipindahkan. Namun, SHN_ABS
juga tidak SHN_ABS
untukku.
Tabel baris
Di sini kita menemukan untuk pertama kalinya tabel string - tabel string yang digunakan dalam file. Bahkan, jika const char *strtab
adalah tabel string, maka nama sh_name
hanyalah strtab + sh_name
. Ya, itu hanya garis yang dimulai dengan indeks tertentu dan berlanjut ke nol byte. Garis-garis mungkin bersilangan (lebih tepatnya, yang satu mungkin merupakan akhiran yang lain). Bagian dapat memiliki nama, maka dalam ELF Header bidang e_shstrndx
akan menunjuk ke bagian dari tabel baris (yang untuk nama bagian, jika ada beberapa), dan bidang sh_name
di header bagian ke baris tertentu.
Bytes pertama (nol) dan terakhir dari tabel baris berisi karakter nol. Yang terakhir dapat dimengerti mengapa: nilai-jam, mengakhiri baris terakhir. Tetapi offset nol menentukan nama tidak ada atau kosong - tergantung pada konteksnya.
Memuat bagian
Ada dua alamat di header setiap bagian: satu, sh_addr
adalah alamat muat (di mana bagian akan ditempatkan dalam memori), yang lain, sh_offset
adalah offset dalam file di mana bagian ini terletak di sana. Saya tidak tahu bagaimana keduanya, tetapi masing-masing nilai-nilai ini secara individual dapat 0: dalam satu kasus, bagian "tetap pada disk", karena ada beberapa jenis informasi layanan. Di bagian lain, bagian tidak dimuat dari disk , misalnya, Anda hanya perlu memilihnya dan memberi skor dengan angka nol ( .bss
). Jujur saja, walaupun saya tidak harus memproses alamat unduhan - di mana itu diunggah, di sana itu diunggah :) Namun, kami memiliki program khusus, terus terang juga.
Relokasi
Dan sekarang hal yang menarik: menurut langkah-langkah keamanan, seperti yang Anda tahu, mereka tidak pergi ke Matrix tanpa operator yang tersisa di pangkalan. Dan karena kita masih memiliki fantasi di sini, koneksi dengan operator akan bersifat telepati. Oh ya, saya mengumumkan lima menit keuletan selesai. Secara umum, kami akan secara singkat membahas proses penautan.
Untuk percobaan saya, saya perlu sepotong kode dikompilasi menjadi boot biasa, dimuat dengan libdl
biasa. Di sini saya bahkan tidak akan menjelaskan secara detail - cukup buka dlopen
, tarik karakter melalui dlsym
, tutup dengan dlclose
ketika program dlclose
. Namun, bahkan ini adalah detail implementasi yang tidak terkait dengan pemuat file ELF kami . Ada beberapa konteks : kemampuan untuk mendapatkan pointer dengan nama.
Secara umum, set instruksi eBPF adalah kemenangan dari kode mesin yang disejajarkan: instruksi selalu membutuhkan 8 byte dan memiliki struktur
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
Selain itu, banyak bidang dalam setiap instruksi spesifik mungkin tidak digunakan - menghemat ruang untuk kode "mesin" bukan tentang kami.
Bahkan, instruksi pertama dapat segera mengikuti yang kedua, yang tidak mengandung opcodes, tetapi hanya memperluas bidang langsung dari 32 hingga 64 bit. Berikut adalah tambalan untuk instruksi majemuk yang disebut R_BPF_64_64
.
Untuk melakukan relokasi, sekali lagi kita akan melihat tabel bagian untuk sh_type == SHT_REL
. Bidang sh_info
dari header akan menunjukkan bagian mana yang kita tambal, dan sh_link
- dari tabel mana untuk mengambil deskripsi karakter.
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
Sebenarnya, ada dua jenis bagian relokasi: REL
dan RELA
- yang kedua secara eksplisit berisi istilah tambahan, tapi saya belum melihatnya, jadi kami hanya menambahkan pernyataan bahwa ia tidak bertemu, dan kami akan memprosesnya. Selanjutnya, saya akan menambah nilai yang tertulis dalam instruksi, alamat simbol. Dan di mana mendapatkannya? Di sini, seperti yang sudah kita ketahui, opsi dimungkinkan:
- Simbol mengacu pada bagian
SHN_ABS
. Maka ambil saja st_value
- Karakter merujuk ke bagian `SHN_UNDEF. Kemudian tarik simbol luar
- Dalam kasus lain, cukup tambal tautan ke bagian lain dari file yang sama`
Cara mencobanya sendiri
Pertama, apa yang harus dibaca? Selain spesifikasi yang sudah ditentukan , masuk akal untuk membaca file ini , di mana tim iovisor mengumpulkan informasi yang diekstrak dari kernel Linux melalui eBPF.
Kedua, bagaimana seharusnya setiap orang bekerja dengan ini? Pertama, Anda perlu mendapatkan file ELF dari suatu tempat. Seperti yang dinyatakan di StackOverfow , tim akan membantu kami.
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
Kedua, Anda perlu entah bagaimana mendapatkan analisis referensi file menjadi beberapa bagian. Dalam situasi normal, perintah objdump
akan membantu kami:
$ objdump : objdump <> <()> <()>. : -a, --archive-headers Display archive header information -f, --file-headers Display the contents of the overall file header -p, --private-headers Display object format specific file header contents -P, --private=OPT,OPT... Display object format specific contents -h, --[section-]headers Display the contents of the section headers -x, --all-headers Display the contents of all headers -d, --disassemble Display assembler contents of executable sections -D, --disassemble-all Display assembler contents of all sections --disassemble=<sym> Display assembler contents from <sym> -S, --source Intermix source code with disassembly -s, --full-contents Display the full contents of all sections requested -g, --debugging Display debug information in object file -e, --debugging-tags Display debug information using ctags style -G, --stabs Display (in raw form) any STABS info in the file -W[lLiaprmfFsoRtUuTgAckK] or --dwarf[=rawline,=decodedline,=info,=abbrev,=pubnames,=aranges,=macro,=frames, =frames-interp,=str,=loc,=Ranges,=pubtypes, =gdb_index,=trace_info,=trace_abbrev,=trace_aranges, =addr,=cu_index,=links,=follow-links] Display DWARF info in the file -t, --syms Display the contents of the symbol table(s) -T, --dynamic-syms Display the contents of the dynamic symbol table -r, --reloc Display the relocation entries in the file -R, --dynamic-reloc Display the dynamic relocation entries in the file @<file> Read options from <file> -v, --version Display this program's version number -i, --info List object formats and architectures supported -H, --help Display this information
Tetapi dalam kasus ini, tidak berdaya:
$ objdump -d test-bpf.o test-bpf.o: elf64-little objdump: UNKNOWN!
Lebih tepatnya, itu akan menampilkan bagian, tetapi pembongkaran adalah masalah. Di sini kita mengingat apa yang kami kumpulkan menggunakan LLVM. LLVM memiliki analog utilities yang diperluas dari binutils, dengan nama form llvm-< >
. Mereka, misalnya, memahami bitcode LLVM. Dan mereka juga mengerti eBPF - pasti tergantung pada opsi kompilasi, tetapi karena dikompilasi, itu mungkin harus selalu diurai. Karena itu, untuk kenyamanan, saya sarankan membuat skrip:
vim test-bpf.c
Kemudian untuk sumber seperti itu:
#include <stdint.h> extern uint64_t z; uint64_t func(uint64_t x, uint64_t y) { return x + y + z; }
Akan ada hasil seperti itu:
$ ./compile-bpf.sh test-bpf.o: file format ELF64-BPF Disassembly of section .text: 0000000000000000 func: 0: bf 20 00 00 00 00 00 00 r0 = r2 1: 0f 10 00 00 00 00 00 00 r0 += r1 2: 18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 r1 = 0 ll 0000000000000010: R_BPF_64_64 z 4: 79 11 00 00 00 00 00 00 r1 = *(u64 *)(r1 + 0) 5: 0f 10 00 00 00 00 00 00 r0 += r1 6: 95 00 00 00 00 00 00 00 exit SYMBOL TABLE: 0000000000000000 l df *ABS* 00000000 test-bpf.c 0000000000000000 ld .text 00000000 .text 0000000000000000 g F .text 00000038 func 0000000000000000 *UND* 00000000 z
Kode
Bagian 1. QInst: lebih baik kehilangan satu hari, kemudian terbang dalam lima menit (alat tulis itu sepele)