تحذير: يحتوي على برمجة النظام. نعم ، في جوهره ، لا يحتوي على أي شيء آخر.
دعنا نتخيل أنك أعطيت مهمة كتابة لعبة خيالية. حسنا ، هناك عن الجان. وحول الواقع الافتراضي. منذ الطفولة ، كنت تحلم بكتابة شيء كهذا ، وبدون تردد ، توافق. سرعان ما تدرك أنك تعرف معظم العالم من الجان من النكات من bashorgh القديم وغيرها من المصادر المتباينة. عفوًا ، مشكلة. حسنًا ، حيث لم تختف ... لدينا تدرس من خلال تجربة برمجة غنية ، تذهب إلى Google ، وأدخل "مواصفات Elf" واتبع الروابط. أوه! هذا واحد يؤدي إلى نوع من PDF ... لذلك ما لدينا هنا ... نوع من Elf32_Sword
- السيوف الجان - يبدو مثل ما تحتاجه. يبدو أن الرقم 32 هو مستوى الشخصية ، ومن المحتمل أن يكون السببان الرابعان في الأعمدة التالية ضررًا. بالضبط ما تحتاجه ، وإلى جانب كيف منهجية! ..
كما ذكر في مهمة برمجة أوليمبياد ، بعد فقرتين من نص مفصل حول موضوع اليابان ، الساموراي واليشا: "كما فهمت بالفعل ، فإن المهمة لن تدور حول ذلك على الإطلاق." أوه نعم ، كانت المسابقة ، بالطبع ، لفترة من الوقت. بشكل عام ، أعلن إغلاق خمس دقائق من مثابرة.
اليوم سأحاول التحدث عن تحليل ملف بتنسيق ELF 64 بت. من حيث المبدأ ، ما لا يخزنونه فيه فقط هو البرامج الأصلية ، والمكتبات الثابتة ، والمكتبات الديناميكية ، وكل تطبيق محدد ، مثل تعطل التطبيقات ... يتم استخدامه ، على سبيل المثال ، على نظام Linux والعديد من الأنظمة الأخرى المشابهة لنظام Unix ، نعم ، كما يقولون ، حتى على الهواتف تم دعمها بنشاط في البرامج الثابتة مصححة من قبل. يبدو أن دعم تنسيق تخزين البرامج من أنظمة تشغيل خطيرة يجب أن يكون صعباً. لذلك اعتقدت. نعم ، ربما هو كذلك. لكننا سندعم حالة استخدام محددة للغاية: تحميل eBPF bytecode من ملفات .o
. لماذا هذا فقط لمزيد من التجارب ، سأحتاج إلى رمز ثنائي أساسي (على سبيل المثال لا أعلى الركبة ) ، يمكن الحصول عليه من C ولا يمكن كتابته يدويًا ، لذلك eBPF بسيط وهناك خلفية LLVM لذلك. وأحتاج فقط إلى تحليل ELF كحاوية يتم فيها وضع هذا الكود بواسطة المترجم.
فقط في حالة ، سوف أوضح: المقال عبارة عن برمجة استكشافية ولا يدعي أنه دليل شامل. الهدف النهائي هو جعل أداة تحميل التشغيل تسمح لك بقراءة برامج C المترجمة في eBPF باستخدام Clang - البرامج المتوفرة لدي - في وحدة تخزين كافية لمواصلة التجارب.
رأس
يبدأ عند الصفر إزاحة في ELF يكمن الرأس. يحتوي على الأحرف E ، L ، F ، والتي يمكن رؤيتها إذا حاولت فتحها باستخدام محرر نصوص ، وبعض المتغيرات العامة. في الواقع ، يكون الرأس هو الهيكل الوحيد في الملف الموجود في إزاحة ثابتة ، ويحتوي على معلومات للعثور على بقية الهيكل. (فيما يلي ، elf.h
تنسيق 32 بت و elf.h
، الذي يعرف عن 64 بت. لذا ، إذا لاحظت أخطاء ، elf.h
تتردد في تصحيحها)
أول ما unsigned char e_ident[16]
في الملف هو unsigned char e_ident[16]
. تذكر هذه المقالات الممتعة في سلسلة "جميع العبارات التالية خاطئة"؟ في ما يلي الأمر نفسه: يمكن أن يحتوي ELF على رمز 32 أو 64 بت ، و Little أو Big Endian ، وحتى عشرات المعالجات. سوف تقرأه كـ Elf64 تحت Little endian - حسنًا ، حظ سعيد ... هذه المجموعة من البايتات هي نوع من توقيع ما بداخلها وكيفية تحليلها.
مع وحدات البايت الأربعة الأولى ، كل شيء بسيط - هو [0x7f, 'E', 'L', 'F']
. إذا لم تتطابق ، فهناك سبب يدعو للاعتقاد بأنها نوع من النحل الخطأ. البايت التالي يحتوي على الفصل شخصية ملف: ELFCLASS32
أو ELFCLASS64
- عمق بت. للبساطة ، سنعمل فقط مع ملفات 64 بت (هل هناك eBPF 32 بت؟). إذا تبين أن ELFCLASS32
هو ELFCLASS32
، فنحن ببساطة نخرج مع وجود خطأ: كل ذلك ، سوف تطفو الهياكل ، ولن يضر فحص التعقل للقيام بذلك. تشير البايتة الأخيرة التي تهمنا في هذا الهيكل إلى نهاية الملف - سنعمل فقط مع ترتيب البايت الأصلي لمعالجنا.
فقط في حالة ، سوف أوضح: عند العمل باستخدام تنسيق ELF في C ، يجب ألا تطرح كل int عن طريق الإزاحة المحسوبة بذكاء - elf.h
يحتوي على الهياكل اللازمة ، وحتى أرقام البايت في e_ident
: EI_MAG0
، EI_MAG1
، EI_MAG2
، EI_MAG3
، EI_CLASS
، EI_DATA
، EI_DATA
فقط مؤشر إلى البيانات من قراءة أو تعيينها في الذاكرة من الملف إلى المؤشر إلى بنية وقراءة.
بالإضافة إلى e_ident
يحتوي الرأس على حقول أخرى ، بعضها سنقوم بالتحقق منه فقط ، وسيتم استخدام بعضها لمزيد من التحليل ، ولكن لاحقًا. أي ، نحن نتحقق من أن e_machine == EM_BPF
(أي ، هو "تحت بنية معالج eBPF") ، e_type == ET_REL
، e_shoff != 0
. الاختيار الأخير له المعنى التالي: يمكن أن يحتوي الملف على معلومات للارتباط (جدول الأقسام والأقسام) أو التشغيل (جدول البرنامج وشرائحه) أو كليهما. من خلال التحققين الأخيرين ، نتحقق من أن المعلومات التي نحتاجها (كما لو للربط) موجودة في الملف. تحقق أيضًا من أن إصدار التنسيق هو EV_CURRENT
.
إبداء تحفظ على الفور ، لن أتحقق من صحة الملف ، على افتراض أننا إذا قمنا بتحميله في عمليتنا ، فإننا نثق به. في كود النواة أو البرامج الأخرى التي تعمل مع ملفات غير موثوق بها ، من المستحيل بطبيعة الحال القيام بذلك في أي حال .
جدول القسم
كما قلت ، نحن مهتمون برؤية طريقة ربط الملف ، أي جدول القسم والأقسام نفسها. توجد معلومات حول مكان البحث عن جدول القسم في الرأس. يشار إلى حجمها أيضًا هناك ، وكذلك حجم عنصر واحد - يمكن أن يكون أكبر من sizeof(Elf64_Shdr)
(لأنه سيؤثر على رقم إصدار التنسيق ، وأنا بصراحة لا أعرف). بعض أرقام الأقسام الرئيسية محجوزة وليست موجودة بالفعل في الجدول. الرجوع إليهم له معنى خاص. يبدو أننا مهتمون بـ SHN_UNDEF
فقط (صفر محجوز أيضًا - القسم المفقود ؛ بالمناسبة ، كما تعلمون ، لا يزال عنوانه في الجدول موجودًا) SHN_ABS
. الرمز "المعرّف في قسم SHN_UNDEF
" غير معرف فعليًا ، وفي SHN_ABS
له قيمة مطلقة ولا يتم نقله. ومع ذلك ، يبدو أن SHN_ABS
ليست SHN_ABS
لي أيضًا.
جدول الصف
نأتي هنا لأول مرة لجداول السلاسل الزمنية - جداول السلاسل المستخدمة في الملف. في الواقع ، إذا كانت const char *strtab
عبارة عن جدول سلاسل ، فإن الاسم sh_name
هو مجرد strtab + sh_name
. نعم ، إنه مجرد خط يبدأ بفهرس معين ويستمر في صفر بايت. قد تتقاطع الخطوط (بتعبير أدق ، قد يكون أحدها لاحقة الآخر). يمكن أن تحتوي الأقسام على أسماء ، ثم في حقل ELF Header ، e_shstrndx
الحقل e_shstrndx
إلى قسم من جدول الصفوف (القسم الخاص بأسماء الأقسام ، إذا كان هناك عدة أسماء) ، sh_name
الحقل في رأس القسم إلى سطر معين.
تحتوي البايتات الأولى (صفر) والأخيرة من جدول الصف على أحرف فارغة. هذا الأخير مفهوم لماذا: قيمة الساعة ، تنتهي السطر الأخير. لكن الإزاحة الصفرية تحدد اسمًا غائبًا أو فارغًا - وفقًا للسياق.
تحميل الاقسام
يوجد sh_addr
في رأس كل قسم: أحدهما ، sh_addr
هو عنوان التحميل (حيث سيتم وضع القسم في الذاكرة) ، والآخر ، sh_offset
هو الإزاحة في الملف الذي يوجد به هذا القسم. لا أعرف كيف هي الأمرين ، ولكن يمكن أن تكون كل من هذه القيم على حدة 0: في حالة واحدة ، يكون القسم "يبقى على القرص" ، لأن هناك نوعًا من معلومات الخدمة. في مكان آخر ، لم يتم تحميل القسم من القرص ، على سبيل المثال ، تحتاج فقط إلى تحديده .bss
باستخدام الأصفار ( .bss
). بصراحة ، رغم أنني لم أضطر إلى معالجة عنوان التنزيل - حيث تم تحميله ، إلا أنه تم تحميله :) ومع ذلك ، لدينا برامج محددة ، بصراحة ، أيضًا.
نقل
والشيء المثير للاهتمام الآن: وفقًا لتدابير السلامة ، كما تعلمون ، لا يذهبون إلى المصفوفة بدون وجود مشغل في القاعدة. ولأننا لا نزال نتخيل هنا ، فإن الاتصال بالمشغل سيكون خائفًا. أوه نعم ، أعلنت خمس دقائق من مثابرة الانتهاء. بشكل عام ، سنناقش بإيجاز عملية الربط.
بالنسبة لتجربتي ، أحتاج إلى جزء من الشفرة تم تجميعه في نظام تشغيل عادي ، محمّل libdl
منتظم. هنا لن dlsym
بالتفصيل - فقط افتح dlopen
، اسحب الشخصيات عبر dlsym
، dlclose
مع dlclose
عندما dlclose
البرنامج. ومع ذلك ، فحتى هذه هي تفاصيل التنفيذ التي لا تتعلق بملف تحميل ELF الخاص بنا . هناك ببساطة بعض السياق : القدرة على الحصول على مؤشر بالاسم.
بشكل عام ، تعد مجموعة تعليمات eBPF بمثابة انتصار لرمز الجهاز المحاذي: تستغرق التعليمة دائمًا 8 بايت وتحتوي على بنية
struct { uint8_t opcode; uint8_t dst:4; uint8_t src:4; uint16_t offset; uint32_t imm; };
علاوة على ذلك ، قد لا يتم استخدام العديد من الحقول في كل تعليمات محددة - توفير مساحة لرمز "الجهاز" لا يخصنا.
في الواقع ، يمكن للتعليمات الأولى أن تتبع مباشرة التعليمة الثانية ، والتي لا تحتوي على أي رموز فيديو ، ولكنها ببساطة تمدد المجال الفوري من 32 إلى 64 بت. فيما يلي تصحيح لمثل هذه التعليمة المركبة تسمى R_BPF_64_64
.
من أجل إجراء النقل ، سننظر مرة أخرى في جدول القسم sh_type == SHT_REL
. sh_info
حقل sh_info
في الرأس إلى القسم الذي sh_link
، و sh_link
- من أي جدول لأخذ وصف للأحرف.
typedef struct { Elf64_Addr r_offset; Elf64_Xword r_info; } Elf64_Rel;
في الواقع ، هناك نوعان من أقسام النقل: REL
و RELA
- يحتوي القسم الثاني صراحةً على مصطلح إضافي ، لكنني لم أره بعد ، لذلك نضيف فقط التأكيد على أنه لا يفي ، وسنعالجه. بعد ذلك ، سأضيف إلى القيمة المكتوبة في التعليمات ، عنوان الرمز. وأين يمكن الحصول عليها؟ هنا ، كما نعلم بالفعل ، الخيارات ممكنة:
- يشير الرمز إلى قسم
SHN_ABS
. ثم فقط تأخذ st_value
- يشير الحرف إلى قسم `SHN_UNDEF. ثم اسحب الرمز الخارجي
- في حالات أخرى ، فقط قم بتصحيح الرابط إلى قسم آخر من نفس الملف
كيف تحاول ذلك بنفسك
أولا ، ماذا تقرأ؟ بالإضافة إلى المواصفات المحددة بالفعل ، من المنطقي قراءة هذا الملف ، حيث يقوم فريق iovisor بجمع المعلومات المستخرجة من kernel Linux عبر eBPF.
وثانيا ، كيف ، في الواقع ، ينبغي أن يعمل الجميع مع هذا؟ تحتاج أولاً إلى الحصول على ملف ELF من مكان ما. كما ورد في StackOverfow ، سيساعدنا الفريق.
clang -O2 -emit-llvm -c bpf.c -o - | llc -march=bpf -filetype=obj -o bpf.o
ثانياً ، تحتاج إلى الحصول على تحليل مرجعي للملف بطريقة أو بأخرى. في الوضع الطبيعي ، objdump
الأمر objdump
:
$ 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
ولكن في هذه الحالة ، فإنه عاجز:
$ objdump -d test-bpf.o test-bpf.o: elf64-little objdump: UNKNOWN!
بتعبير أدق ، سوف يعرض أقسامًا ، لكن تفكيكها يمثل مشكلة. هنا نتذكر ما جمعناه باستخدام LLVM. لدى LLVM نظائرها الموسعة الخاصة بالمرافق من binutils ، مع أسماء النموذج llvm-< >
. فهم ، على سبيل المثال ، يفهمون كود LLVM. كما أنهم يفهمون eBPF - بالتأكيد يعتمد على خيارات الترجمة ، ولكن نظرًا لأنه مترجم ، فمن المحتمل أن يتم تحليله دائمًا. لذلك ، للراحة ، أوصي بإنشاء برنامج نصي:
vim test-bpf.c
ثم لمثل هذا المصدر:
#include <stdint.h> extern uint64_t z; uint64_t func(uint64_t x, uint64_t y) { return x + y + z; }
ستكون هناك مثل هذه النتيجة:
$ ./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
كود .
الجزء 1. QInst: من الأفضل أن تخسر يومًا ، ثم تطير في خمس دقائق (أدوات الكتابة تافهة)