الجزء 0. يتطلب قزم للعمل في المصفوفة. نقل ممكن

تحذير: يحتوي على برمجة النظام. نعم ، في جوهره ، لا يحتوي على أي شيء آخر.


دعنا نتخيل أنك أعطيت مهمة كتابة لعبة خيالية. حسنا ، هناك عن الجان. وحول الواقع الافتراضي. منذ الطفولة ، كنت تحلم بكتابة شيء كهذا ، وبدون تردد ، توافق. سرعان ما تدرك أنك تعرف معظم العالم من الجان من النكات من 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 #     clang -Oz -emit-llvm -c test-bpf.c -o - | llc -march=bpf -filetype=obj -o test-bpf.o llvm-objdump -d -t -r test-bpf.o 

ثم لمثل هذا المصدر:


 #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: من الأفضل أن تخسر يومًا ، ثم تطير في خمس دقائق (أدوات الكتابة تافهة)

Source: https://habr.com/ru/post/ar452592/


All Articles