فهم نماذج كود العمارة x64

"ما هو نموذج الرمز الذي يجب استخدامه؟" - سؤال ناشئ بشكل متكرر ، ولكن نادرًا ما يتم التعامل معه عند كتابة التعليمات البرمجية لمعمارية x64. ومع ذلك ، فهذه مشكلة مثيرة للاهتمام إلى حد ما ، ومن المفيد أن تكون لديك فكرة عن نماذج الكود لفهم كود آلة x64 الذي تم إنشاؤه بواسطة المترجمين. بالإضافة إلى ذلك ، بالنسبة لأولئك الذين يهتمون بالأداء وصولًا إلى أصغر الأوامر ، يؤثر اختيار نموذج الرمز أيضًا على التحسين.



المعلومات حول هذا الموضوع على الشبكة ، أو في أي مكان آخر ، نادرة. أهم الموارد المتاحة هو x64 ABI الرسمي ، يمكنك تنزيله من هنا (سيشار إليه فيما بعد باسم "ABI"). يمكن العثور على بعض المعلومات على manصفحاتgcc... الهدف من هذه المقالة هو تقديم توصيات يمكن الوصول إليها حول الموضوع ، ومناقشة القضايا ذات الصلة ، وكذلك إظهار بعض المفاهيم من خلال الكود المستخدم في العمل مع أمثلة جيدة.



ملاحظة مهمة: ليس المقصود من هذه المقالة أن تكون تعليميًا للمبتدئين. قبل التعارف ، من المستحسن أن يكون لديك أمر قوي من C و المجمع ، بالإضافة إلى معرفة أساسية بهندسة x64.






انظر أيضًا مشاركتنا السابقة حول موضوع ذي صلة : كيف يعالج x86_x64 الذاكرة






نماذج الكود. الجزء التحفيزي



في معمارية x64 ، يتم إرسال كل من الكود والبيانات عبر نماذج عنونة نسبية للأوامر (أو باستخدام مصطلحات x64 بلغة RIP). في هذه الأوامر ، يقتصر التحول من RIP على 32 بت ، ولكن قد تكون هناك حالات عندما لا يكون لدى الأمر ، عند محاولة معالجة جزء من الذاكرة أو البيانات ، ما يكفي من التحول من 32 بت ، على سبيل المثال ، عند العمل مع البرامج أكثر من غيغابايت.



إحدى الطرق لحل هذه المشكلة هي التخلي تمامًا عن وضع العنونة النسبي لـ RIP لصالح التحول الكامل 64 بت لجميع مراجع البيانات والرموز. ومع ذلك ، فإن هذه الخطوة مكلفة للغاية: لتغطية الحالة (النادرة نوعًا ما) للبرامج والمكتبات الكبيرة بشكل لا يصدق ، حتى أبسط العمليات داخل الكود بأكمله تتطلب تعليمات أكثر من المعتاد.



وبالتالي ، تصبح نماذج الكود حل وسط. [1] نموذج الكود هو اتفاق رسمي بين المبرمج والمترجم حيث يحدد المبرمج نواياه حول حجم البرنامج (أو البرامج) المتوقع الذي سيحتوي على وحدة الكائن التي يتم تجميعها حاليًا. [2] هناك حاجة إلى نماذج كود حتى يتمكن المبرمج من إخبار المترجم: "لا تقلق ، فإن وحدة الكائن هذه ستذهب فقط إلى البرامج الصغيرة ، لذا يمكنك استخدام أوضاع العنونة النسبية السريعة لـ RIP". من ناحية أخرى ، قد تخبر المترجم بما يلي: "سنقوم بربط هذه الوحدة ببرامج كبيرة ، لذا يرجى استخدام أوضاع العناوين المطلقة والآمنة على مهل مع التحول الكامل 64 بت."



ما ستتحدث عنه هذه المقالة



سنتحدث عن السيناريوهين الموصوفين أعلاه ، نموذج رمز صغير ونموذج رمز كبير: النموذج الأول يخبر المترجم أن التحول النسبي 32 بت يجب أن يكون كافياً لجميع المراجع إلى التعليمات البرمجية والبيانات في وحدة الكائن ؛ الثاني يصر على أن المترجم يستخدم أوضاع عنونة 64 بت مطلق. بالإضافة إلى ذلك ، هناك أيضًا نسخة وسيطة ، ما يسمى بنموذج الرمز الأوسط .



يتم تقديم كل نموذج من نماذج الرموز هذه في اختلافات PIC وغير PIC المستقلة ، وسوف نتحدث عن كل من النماذج الستة.



مثال C الأصلي



لتوضيح المفاهيم التي تمت مناقشتها في هذه المقالة ، سأستخدم برنامج C أدناه وأجمعه مع نماذج التعليمات البرمجية المختلفة. كما ترى ، mainتصل الوظيفة إلى أربعة صفائف عالمية مختلفة ووظيفة عالمية واحدة. تختلف المصفوفات في معلمتين: الحجم والرؤية. الحجم مهم لشرح متوسط ​​نموذج الكود ولن تكون هناك حاجة للعمل مع النماذج الصغيرة والكبيرة. الرؤية مهمة لتشغيل نماذج رمز PIC وهي إما ثابتة (مرئية فقط في الملف المصدر) أو عالمية (الرؤية لجميع الكائنات المرتبطة بالبرنامج).



int global_arr[100] = {2, 3};
static int static_arr[100] = {9, 7};
int global_arr_big[50000] = {5, 6};
static int static_arr_big[50000] = {10, 20};

int global_func(int param)
{
    return param * 10;
}

int main(int argc, const char* argv[])
{
    int t = global_func(argc);
    t += global_arr[7];
    t += static_arr[7];
    t += global_arr_big[7];
    t += static_arr_big[7];
    return t;
}


gccيستخدم نموذج الشفرة كقيمة الخيار -mcmodel. بالإضافة إلى ذلك ، -fpicيمكن تعيين تجميع PIC بعلامة .



مثال على التجميع في وحدة كائن من خلال نموذج رمز كبير باستخدام PIC:



> gcc -g -O0 -c codemodel1.c -fpic -mcmodel=large -o codemodel1_large_pic.o


نموذج كود صغير



ترجمة اقتباس من man gcc على نموذج كود صغير:



-mcmodel =

توليد كود صغير لنموذج صغير: يجب ربط البرنامج ورموزه في الجيجاين السفليين من مساحة العنوان. حجم المؤشرات 64 بت. يمكن بناء البرامج بشكل ثابت وديناميكي. هذا هو نموذج الكود الأساسي.




بمعنى آخر ، يمكن للمترجم أن يفترض بأمان أنه يمكن الوصول إلى التعليمات البرمجية والبيانات عبر إزاحة نسبية RIP 32 بت من أي أمر في التعليمات البرمجية. دعونا نلقي نظرة على مثال مفكك لبرنامج C قمنا بتجميعه من خلال نموذج رمز صغير غير PIC:



> objdump -dS codemodel1_small.o
[...]
int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 48 83 ec 20             sub    $0x20,%rsp
  1d: 89 7d ec                mov    %edi,-0x14(%rbp)
  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24: 8b 45 ec                mov    -0x14(%rbp),%eax
  27: 89 c7                   mov    %eax,%edi
  29: b8 00 00 00 00          mov    $0x0,%eax
  2e: e8 00 00 00 00          callq  33 <main+0x1e>
  33: 89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  3c: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  45: 01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  48: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  4e: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  51: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  57: 01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  5a: 8b 45 fc                mov    -0x4(%rbp),%eax
}
  5d: c9                      leaveq
  5e: c3                      retq


كما ترى ، يتم تنظيم الوصول إلى جميع المصفوفات بنفس الطريقة - باستخدام التحول النسبي لـ RIP. ومع ذلك ، في الكود ، يكون التحول هو 0 ، لأن المترجم لا يعرف مكان وضع قطعة البيانات ، لذلك لكل عملية وصول ، يقوم بإنشاء نقل:



> readelf -r codemodel1_small.o

Relocation section '.rela.text' at offset 0x62bd8 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000002f  001500000002 R_X86_64_PC32     0000000000000000 global_func - 4
000000000038  001100000002 R_X86_64_PC32     0000000000000000 global_arr + 18
000000000041  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8
00000000004a  001200000002 R_X86_64_PC32     0000000000000340 global_arr_big + 18
000000000053  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098


دعونا ، كمثال ، فك شفرة الوصول إلى global_arr. الجزء المفكك الذي يهمنا هو:



  t += global_arr[7];
36:       8b 05 00 00 00 00       mov    0x0(%rip),%eax
3c:       01 45 fc                add    %eax,-0x4(%rbp)


يعتبر العنوان النسبي لـ RIP نسبيًا للأمر التالي ، لذا يجب تصحيح movالتصحيح في الأمر بحيث يتوافق مع 0x3s. نحن مهتمون بالانتقال الثاني ، R_X86_64_PC32فهو يشير إلى المُعامل movعلى العنوان 0x38ويعني ما يلي: نأخذ قيمة الرمز ، ونضيف المصطلح ونطرح التحول الذي يشير إليه النقل. إذا قمت بحساب كل شيء بشكل صحيح ، فسوف ترى كيف ستؤدي النتيجة إلى تحول نسبي بين الأمر التالي global_arr، بالإضافة إلى 01. نظرًا لأنه 01يعني "int السابع في الصفيف" (في بنية x64 حجم كل int4 بايت) ، فإننا نحتاج هذا التحول النسبي. وبالتالي ، باستخدام العنوان النسبي RIP ، يشير الأمر بشكل صحيح global_arr[7].



من المثير للاهتمام أيضًا ملاحظة ما يلي: على الرغم من أن أوامر الوصول static_arrمتشابهة هنا ، إلا أن إعادة التوجيه تستخدم حرفًا مختلفًا ، وبالتالي الإشارة إلى قسم بدلاً من حرف معين .data. ويرجع ذلك إلى إجراءات الرابط ، فهو يضع مصفوفة ثابتة في مكان معروف في القسم ، وبالتالي لا يمكن استخدام الصفيف مع المكتبات المشتركة الأخرى. ونتيجة لذلك ، سيقوم الرابط بحل الموقف مع عملية النقل هذه. من ناحية أخرى ، نظرًا لأنه global_arrيمكن استخدامه (أو الكتابة فوقه) من قبل مكتبة مشتركة أخرى ، سيتعين على المحمل الديناميكي بالفعل معرفة الرابط global_arr. [3]



أخيرًا ، فلنلقِ نظرة على الإشارة إلى global_func:



  int t = global_func(argc);
24:       8b 45 ec                mov    -0x14(%rbp),%eax
27:       89 c7                   mov    %eax,%edi
29:       b8 00 00 00 00          mov    $0x0,%eax
2e:       e8 00 00 00 00          callq  33 <main+0x1e>
33:       89 45 fc                mov    %eax,-0x4(%rbp)


نظرًا لأن المُعامل هو callqأيضًا نسبيًا لـ RIP ، فإن عملية النقل R_X86_64_PC32تعمل هنا بشكل مماثل لوضع التحول النسبي الفعلي إلى global_func في المعامل.



في الختام ، نلاحظ أنه نظرًا لنموذج الرمز الصغير ، فإن المترجم يرى جميع البيانات والشفرة الخاصة بالبرنامج المستقبلي على أنها متاحة من خلال التحول 32 بت ، وبالتالي ينشئ رمزًا بسيطًا وفعالًا للوصول إلى جميع أنواع الكائنات.



نموذج رمز كبير



ترجمة الاقتباسات من man gccموضوع نموذج الكود الكبير:



-mcmodel =

رمز توليد كبير لنموذج كبير: لا يقدم هذا النموذج أي افتراضات حول العناوين وأحجام الأقسام.


مثال على كود مفكك mainتم تجميعه مع نموذج كبير غير PIC:



int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 48 83 ec 20             sub    $0x20,%rsp
  1d: 89 7d ec                mov    %edi,-0x14(%rbp)
  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24: 8b 45 ec                mov    -0x14(%rbp),%eax
  27: 89 c7                   mov    %eax,%edi
  29: b8 00 00 00 00          mov    $0x0,%eax
  2e: 48 ba 00 00 00 00 00    movabs $0x0,%rdx
  35: 00 00 00
  38: ff d2                   callq  *%rdx
  3a: 89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  3d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  44: 00 00 00
  47: 8b 40 1c                mov    0x1c(%rax),%eax
  4a: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  4d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  54: 00 00 00
  57: 8b 40 1c                mov    0x1c(%rax),%eax
  5a: 01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  5d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  64: 00 00 00
  67: 8b 40 1c                mov    0x1c(%rax),%eax
  6a: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  6d: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  74: 00 00 00
  77: 8b 40 1c                mov    0x1c(%rax),%eax
  7a: 01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  7d: 8b 45 fc                mov    -0x4(%rbp),%eax
}
  80: c9                      leaveq
  81: c3                      retq


مرة أخرى ، من المفيد النظر إلى عمليات الترحيل:



Relocation section '.rela.text' at offset 0x62c18 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000030  001500000001 R_X86_64_64       0000000000000000 global_func + 0
00000000003f  001100000001 R_X86_64_64       0000000000000000 global_arr + 0
00000000004f  000300000001 R_X86_64_64       0000000000000000 .data + 1a0
00000000005f  001200000001 R_X86_64_64       0000000000000340 global_arr_big + 0
00000000006f  000300000001 R_X86_64_64       0000000000000000 .data + 31080


نظرًا لعدم وجود حاجة لإجراء افتراضات حول أحجام أقسام الكود والبيانات ، فإن نموذج الكود الكبير موحد إلى حد ما ويحدد الوصول إلى جميع البيانات بنفس الطريقة. دعونا نلقي نظرة على global_arr:



  t += global_arr[7];
3d:       48 b8 00 00 00 00 00    movabs $0x0,%rax
44:       00 00 00
47:       8b 40 1c                mov    0x1c(%rax),%eax
4a:       01 45 fc                add    %eax,-0x4(%rbp)


يحتاج فريقان إلى الحصول على القيمة المطلوبة من الصفيف. يضع الأمر الأول عنوان 64 بت مطلقًا rax، والذي ، كما سنرى قريبًا ، يتحول إلى عنوان global_arr، بينما يقوم الأمر الثاني بتحميل كلمة من (rax) + 01إلى eax.



لذلك دعونا نركز على الفريق في 0x3d، movabsمطلق إصدار 64 بت movفي الهندسة المعمارية إلى x64. يمكن أن يرمي ثابت 64 بت كامل مباشرة في السجل ، وبما أن قيمة هذا الثابت في الشفرة المفككة تساوي الصفر ، سيتعين علينا الانتقال إلى جدول النقل للحصول على إجابة. في ذلك ، سنجد الانتقال المطلق R_X86_64_64للمعامل في العنوان 0x3f، مع القيمة التالية: وضع قيمة الرمز بالإضافة إلى الملخص مرة أخرى في التحول. بعبارات أخرى،raxسيحتوي على عنوان مطلق global_arr.



ماذا عن وظيفة الاتصال؟



  int t = global_func(argc);
24:       8b 45 ec                mov    -0x14(%rbp),%eax
27:       89 c7                   mov    %eax,%edi
29:       b8 00 00 00 00          mov    $0x0,%eax
2e:       48 ba 00 00 00 00 00    movabs $0x0,%rdx
35:       00 00 00
38:       ff d2                   callq  *%rdx
3a:       89 45 fc                mov    %eax,-0x4(%rbp)


الأمر الذي نعرفه بالفعل movabsمتبوعًا بأمر callيستدعي وظيفة في العنوان في rdx. يكفي النظر إلى عملية النقل المقابلة لفهم مدى تشابهها مع الوصول إلى البيانات.



كما ترى ، لا يقوم نموذج الكود الكبير بأي افتراضات حول حجم الكود وأقسام البيانات ، وكذلك حول الموقع النهائي للأحرف ، فهو يشير ببساطة إلى الأحرف من خلال خطوات 64 بت المطلقة ، وهو نوع من "المسار الآمن". ومع ذلك ، لاحظ كيف ، مقارنة بنموذج الرمز الصغير ، يضطر النموذج الكبير إلى استخدام أمر إضافي لكل حرف. هذا هو ثمن الأمن.



لذا ، التقينا بك نموذجين متعاكسين تمامًا: في حين يفترض نموذج الرمز الصغير أن كل شيء يتناسب مع الجيجابايت السفلية من الذاكرة ، يفترض النموذج الكبير أنه لا يوجد شيء مستحيل ويمكن لأي شخصية أن تكون في أي مكان بالكامل 64- مساحة عنوان بت. المقايضة بين الاثنين هو نموذج الرمز الأوسط.



نموذج كود متوسط



كما كان من قبل ، دعنا نلقي نظرة على ترجمة الاقتباس من man gcc:



-mcmodel=medium

: . . , -mlarge-data-threshold, bss . , .


مشابه لنموذج الكود الصغير ، يفترض النموذج الأوسط أن الكود بأكمله مرتب في جيجابايتين أقل. ومع ذلك ، يتم تقسيم البيانات إلى المفترض أنها مرتبة في الجيجابايت السفلية من "البيانات الصغيرة" وغير محدودة في الذاكرة "البيانات الكبيرة". تصنف البيانات على أنها كبيرة عندما تتجاوز الحد ، وهو تعريف 64 كيلوبايت.



من المهم أيضًا ملاحظة أنه عند العمل باستخدام نموذج رمز متوسط ​​للبيانات الضخمة ، عن طريق القياس مع الأقسام .dataو .bss، يتم إنشاء أقسام خاصة: .ldataو .lbss. هذا ليس مهمًا جدًا في منظور موضوع المقالة الحالية ، لكنني سأنحرف عنه قليلاً. يمكن العثور على مزيد من التفاصيل حول هذه المشكلة في ABI.



الآن أصبح من الواضح لماذا ظهرت هذه المصفوفات في المثال_big: يحتاجها النموذج الأوسط لتفسير "البيانات الضخمة" التي يبلغ حجمها 200 كيلوبايت لكل منها. أدناه يمكنك أن ترى نتيجة التفكيك:



int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 48 83 ec 20             sub    $0x20,%rsp
  1d: 89 7d ec                mov    %edi,-0x14(%rbp)
  20: 48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24: 8b 45 ec                mov    -0x14(%rbp),%eax
  27: 89 c7                   mov    %eax,%edi
  29: b8 00 00 00 00          mov    $0x0,%eax
  2e: e8 00 00 00 00          callq  33 <main+0x1e>
  33: 89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  36: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  3c: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  3f: 8b 05 00 00 00 00       mov    0x0(%rip),%eax
  45: 01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  48: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  4f: 00 00 00
  52: 8b 40 1c                mov    0x1c(%rax),%eax
  55: 01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  58: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  5f: 00 00 00
  62: 8b 40 1c                mov    0x1c(%rax),%eax
  65: 01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  68: 8b 45 fc                mov    -0x4(%rbp),%eax
}
  6b: c9                      leaveq
  6c: c3                      retq


انتبه إلى كيفية الوصول إلى المصفوفات: يتم الوصول إلى المصفوفات _bigمن خلال طرق نموذج الرمز الكبير ، في حين يتم الوصول إلى باقي المصفوفات من خلال طرق النموذج الصغير. تُسمى الوظيفة أيضًا باستخدام طريقة نموذج الرمز الصغير ، وتتشابه عمليات الترحيل كثيرًا مع الأمثلة السابقة لدرجة أنني لن أقوم بعرضها.



نموذج الرمز الأوسط هو حل وسط ماهر بين النماذج الكبيرة والصغيرة. من غير المحتمل أن يتحول رمز البرنامج إلى حجم كبير جدًا [4] ، لذا من الممكن نقل أجزاء كبيرة من البيانات مرتبة بشكل ثابت فيه إلى ما يتجاوز حد الجيجابايت اثنين ، ربما كجزء من بعض البحث الضخم في الجدول. نظرًا لأن نموذج الرمز العادي يقوم بتصفية هذه الكميات الكبيرة من البيانات ويعالجها بطريقة خاصة ، فإن الاستدعاءات إلى الوظائف والأحرف الصغيرة من خلال الكود ستكون فعالة تمامًا كما هو الحال في نموذج الكود الصغير. فقط الوصول إلى الرموز الكبيرة ، عن طريق القياس مع النموذج الكبير ، سيتطلب الرمز لاستخدام طريقة 64 بت الكاملة للنموذج الكبير.



نموذج رمز الموافقة المسبقة عن علم الصغيرة



الآن دعونا نلقي نظرة على خيارات نموذج رمز الموافقة المسبقة عن علم ، وكما كان الحال من قبل ، سنبدأ بنموذج صغير. [5] يمكنك أن ترى أدناه مثالاً على كود تم تجميعه من خلال نموذج PIC صغير:



int main(int argc, const char* argv[])
{
  15:   55                      push   %rbp
  16:   48 89 e5                mov    %rsp,%rbp
  19:   48 83 ec 20             sub    $0x20,%rsp
  1d:   89 7d ec                mov    %edi,-0x14(%rbp)
  20:   48 89 75 e0             mov    %rsi,-0x20(%rbp)
    int t = global_func(argc);
  24:   8b 45 ec                mov    -0x14(%rbp),%eax
  27:   89 c7                   mov    %eax,%edi
  29:   b8 00 00 00 00          mov    $0x0,%eax
  2e:   e8 00 00 00 00          callq  33 <main+0x1e>
  33:   89 45 fc                mov    %eax,-0x4(%rbp)
    t += global_arr[7];
  36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  3d:   8b 40 1c                mov    0x1c(%rax),%eax
  40:   01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr[7];
  43:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
  49:   01 45 fc                add    %eax,-0x4(%rbp)
    t += global_arr_big[7];
  4c:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  53:   8b 40 1c                mov    0x1c(%rax),%eax
  56:   01 45 fc                add    %eax,-0x4(%rbp)
    t += static_arr_big[7];
  59:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
  5f:   01 45 fc                add    %eax,-0x4(%rbp)
    return t;
  62:   8b 45 fc                mov    -0x4(%rbp),%eax
}
  65:   c9                      leaveq
  66:   c3                      retq


الانتقال:



Relocation section '.rela.text' at offset 0x62ce8 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000002f  001600000004 R_X86_64_PLT32    0000000000000000 global_func - 4
000000000039  001100000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
000000000045  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8
00000000004f  001200000009 R_X86_64_GOTPCREL 0000000000000340 global_arr_big - 4
00000000005b  000300000002 R_X86_64_PC32     0000000000000000 .data + 31098


نظرًا لأن الاختلافات بين البيانات الكبيرة والصغيرة لا تلعب أي دور في نموذج الرمز الصغير ، فسنركز على النقاط المهمة عند إنشاء رمز من خلال الموافقة المسبقة عن علم: الاختلافات بين الرموز المحلية (الثابتة) والرموز العالمية.



كما ترى ، لا يوجد فرق بين الرمز الذي تم إنشاؤه للمصفوفات الثابتة والرمز في حالة غير PIC. هذه إحدى مزايا بنية x64: بفضل الوصول النسبي إلى IP للبيانات ، نحصل على الموافقة المسبقة عن علم كمكافأة ، على الأقل حتى يطلب الوصول الخارجي إلى الرموز. تظل جميع الأوامر وعمليات الترحيل كما هي ، لذلك ليست هناك حاجة لمعالجتها مرة أخرى.



من المثير للاهتمام الانتباه إلى المصفوفات العالمية: من الجدير بالذكر أنه في البيانات العامة للموافقة المسبقة عن علم يجب أن تمر عبر GOT ، لأنه في مرحلة ما يمكن تخزينها أو مشاركتها من قبل المكتبات المشتركة [6]. أدناه يمكنك رؤية الرمز للوصول global_arr:



  t += global_arr[7];
36:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
3d:   8b 40 1c                mov    0x1c(%rax),%eax
40:   01 45 fc                add    %eax,-0x4(%rbp)


النقل الذي يهمنا هو R_X86_64_GOTPCREL: موضع إدخال الرمز في GOT بالإضافة إلى المصطلح ، ناقص التحول لتطبيق النقل. بمعنى آخر ، يقوم الأمر بتصحيح الإزاحة النسبية بين RIP (التعليمات التالية) global_arrوالفتحة المخصصة لـ GOT. وبالتالي ، يتم وضع العنوان الفعلي raxفي الأمر على 0x36العنوان global_arr. بعد هذه الخطوة ، يتم إعادة تعيين الرابط إلى العنوان global_arrبالإضافة إلى التحول إلى العنصر السابع في eax.



الآن دعونا نلقي نظرة على استدعاء الوظيفة:



  int t = global_func(argc);
24:   8b 45 ec                mov    -0x14(%rbp),%eax
27:   89 c7                   mov    %eax,%edi
29:   b8 00 00 00 00          mov    $0x0,%eax
2e:   e8 00 00 00 00          callq  33 <main+0x1e>
33:   89 45 fc                mov    %eax,-0x4(%rbp)


يحتوي على نقل callqعنوان المعامل 0x2e، R_X86_64_PLT32: عنوان إدخال PLT للرمز بالإضافة إلى التحول السلبي للمصطلح من أجل تطبيق النقل. وبعبارة أخرى ، callqيجب على PLT استدعاء الترامبولين بشكل صحيح global_func.



لاحظ ما هي الافتراضات الضمنية التي يقدمها المحول البرمجي: أنه يمكن الوصول إلى GOT و PLT من خلال العنونة النسبية لـ RIP. سيكون هذا مهمًا عند مقارنة هذا النموذج مع متغيرات نموذج رمز PIC الأخرى.



نموذج رمز الموافقة المسبقة عن علم كبير



تفكيك:



int main(int argc, const char* argv[])
{
  15: 55                      push   %rbp
  16: 48 89 e5                mov    %rsp,%rbp
  19: 53                      push   %rbx
  1a: 48 83 ec 28             sub    $0x28,%rsp
  1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx
  25: 49 bb 00 00 00 00 00    movabs $0x0,%r11
  2c: 00 00 00
  2f: 4c 01 db                add    %r11,%rbx
  32: 89 7d dc                mov    %edi,-0x24(%rbp)
  35: 48 89 75 d0             mov    %rsi,-0x30(%rbp)
    int t = global_func(argc);
  39: 8b 45 dc                mov    -0x24(%rbp),%eax
  3c: 89 c7                   mov    %eax,%edi
  3e: b8 00 00 00 00          mov    $0x0,%eax
  43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx
  4a: 00 00 00
  4d: 48 01 da                add    %rbx,%rdx
  50: ff d2                   callq  *%rdx
  52: 89 45 ec                mov    %eax,-0x14(%rbp)
    t += global_arr[7];
  55: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  5c: 00 00 00
  5f: 48 8b 04 03             mov    (%rbx,%rax,1),%rax
  63: 8b 40 1c                mov    0x1c(%rax),%eax
  66: 01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr[7];
  69: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  70: 00 00 00
  73: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
  77: 01 45 ec                add    %eax,-0x14(%rbp)
    t += global_arr_big[7];
  7a: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  81: 00 00 00
  84: 48 8b 04 03             mov    (%rbx,%rax,1),%rax
  88: 8b 40 1c                mov    0x1c(%rax),%eax
  8b: 01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr_big[7];
  8e: 48 b8 00 00 00 00 00    movabs $0x0,%rax
  95: 00 00 00
  98: 8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
  9c: 01 45 ec                add    %eax,-0x14(%rbp)
    return t;
  9f: 8b 45 ec                mov    -0x14(%rbp),%eax
}
  a2: 48 83 c4 28             add    $0x28,%rsp
  a6: 5b                      pop    %rbx
  a7: c9                      leaveq
  a8: c3                      retq


الانتقال: هذه المرة ، لا تزال الاختلافات بين البيانات الكبيرة والصغيرة غير مهمة ، لذلك سنركز على و . ولكن عليك أولاً الانتباه إلى المقدمة في هذا الرمز ، في السابق لم نواجه هذا:



Relocation section '.rela.text' at offset 0x62c70 contains 6 entries:

Offset Info Type Sym. Value Sym. Name + Addend

000000000027 00150000001d R_X86_64_GOTPC64 0000000000000000 _GLOBAL_OFFSET_TABLE_ + 9

000000000045 00160000001f R_X86_64_PLTOFF64 0000000000000000 global_func + 0

000000000057 00110000001b R_X86_64_GOT64 0000000000000000 global_arr + 0

00000000006b 000800000019 R_X86_64_GOTOFF64 00000000000001a0 static_arr + 0

00000000007c 00120000001b R_X86_64_GOT64 0000000000000340 global_arr_big + 0

000000000090 000900000019 R_X86_64_GOTOFF64 0000000000031080 static_arr_big + 0


static_arrglobal_arr



1e: 48 8d 1d f9 ff ff ff    lea    -0x7(%rip),%rbx
25: 49 bb 00 00 00 00 00    movabs $0x0,%r11
2c: 00 00 00
2f: 4c 01 db                add    %r11,%rbx


أدناه يمكنك قراءة ترجمة الاقتباس ذي الصلة من ABI:



( GOT) AMD64 IP- . GOT . GOT , AMD64 ISA 32 .


دعونا نلقي نظرة على كيفية حساب المقدمة المذكورة أعلاه عنوان GOT. أولاً ، يقوم الأمر الموجود على العنوان 0x1eبتحميل عنوانه الخاص في rbx. ثم ، مع النقل ، R_X86_64_GOTPC64يتم تنفيذ خطوة 64 بت مطلقة r11. يعني هذا النقل ما يلي: خذ عنوان GOT ، اطرح التحول المنقول وأضف المصطلح. أخيرًا ، 0x2fيضيف الأمر في العنوان كلا النتائج معًا. والنتيجة هي عنوان GOT المطلق rbx. [7]



لماذا تهتم بحساب عنوان GOT؟ أولاً ، كما هو موضح في الاقتباس ، في نموذج رمز كبير ، لا يمكننا أن نفترض أن الإزاحة النسبية لـ 32 بت RIP ستكون كافية لمعالجة GOT ، وهذا هو السبب في أننا بحاجة إلى عنوان 64 بت كامل. ثانيًا ، ما زلنا نريد العمل مع اختلاف PIC ، لذلك لا يمكننا ببساطة وضع العنوان المطلق في التسجيل. بدلاً من ذلك ، يجب حساب العنوان نفسه نسبة إلى RIP. لهذا ، نحن بحاجة إلى مقدمة: فهي تقوم بحساب 64-بت نسبيًا لـ RIP.



على أي حال ، نظرًا لأن لدينا rbxالآن عنوان GOT ، فلنلق نظرة على كيفية الوصول static_arr:



  t += static_arr[7];
69:       48 b8 00 00 00 00 00    movabs $0x0,%rax
70:       00 00 00
73:       8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
77:       01 45 ec                add    %eax,-0x14(%rbp)


نقل الأمر الأول هو R_X86_64_GOTOFF64: الرمز بالإضافة إلى مصطلح GOT ناقص. في حالتنا ، هذا هو الإزاحة النسبية بين العنوان وعنوان static_arrGOT. تضيف التعليمات التالية النتيجة إلى rbx(عنوان GOT المطلق) وتعيد تعيين الإزاحة حسب المرجع 0x1c. لسهولة تصور مثل هذا الحساب ، يمكنك أن ترى أدناه مثال p-pseudo-C:



// char* static_arr
// char* GOT
rax = static_arr + 0 - GOT;  // rax now contains an offset
eax = *(rbx + rax + 0x1c);   // rbx == GOT, so eax now contains
                             // *(GOT + static_arr - GOT + 0x1c) or
                             // *(static_arr + 0x1c)


لاحظ نقطة مثيرة للاهتمام: يتم استخدام عنوان GOT كربط إلى static_arr. عادة ، لا تحتوي GOT على عنوان الرمز ، وبما أنه static_arrليس رمزًا خارجيًا ، فلا يوجد سبب لتخزينه داخل GOT. ومع ذلك ، في هذه الحالة ، يتم استخدام GOT كربط لعنوان الرمز النسبي لقسم البيانات. يمكن العثور على هذا العنوان ، الذي يعد ، من بين أمور أخرى ، موقعًا مستقلاً ، مع تحول كامل 64 بت. الرابط قادر على التعامل مع هذا النقل ، لذلك ليست هناك حاجة لتعديل قسم التعليمات البرمجية في وقت التحميل.



ولكن ماذا عن global_arr؟



  t += global_arr[7];
55:       48 b8 00 00 00 00 00    movabs $0x0,%rax
5c:       00 00 00
5f:       48 8b 04 03             mov    (%rbx,%rax,1),%rax
63:       8b 40 1c                mov    0x1c(%rax),%eax
66:       01 45 ec                add    %eax,-0x14(%rbp)


هذا الرمز أطول قليلاً ، ويختلف النقل عن المعتاد. في الواقع، يتم استخدام GOT هنا بطريقة أكثر تقليدية: نقل R_X86_64_GOT64ل movabsمجرد يحكي وظيفة لوضع الإزاحة في GOT حيث raxيقع عنوان global_arr. 0x5fيأخذ الأمر في العنوان العنوان global_arrمن GOT ويضعه فيه rax. يقوم الأمر التالي بإعادة تعيين المرجع global_arr[7]ووضع القيمة فيه eax.



الآن دعونا نلقي نظرة على رابط الكود global_func. تذكر أنه في نموذج الكود الكبير ، لم نتمكن من وضع افتراضات حول حجم أقسام الكود ، لذا يجب أن نفترض أنه حتى للوصول إلى PLT ، نحتاج إلى عنوان 64 بت مطلق:



  int t = global_func(argc);
39: 8b 45 dc                mov    -0x24(%rbp),%eax
3c: 89 c7                   mov    %eax,%edi
3e: b8 00 00 00 00          mov    $0x0,%eax
43: 48 ba 00 00 00 00 00    movabs $0x0,%rdx
4a: 00 00 00
4d: 48 01 da                add    %rbx,%rdx
50: ff d2                   callq  *%rdx
52: 89 45 ec                mov    %eax,-0x14(%rbp)


النقل الذي يهمنا هو R_X86_64_PLTOFF64: global_funcعنوان إدخال PLT ناقص عنوان GOT. يتم وضع النتيجة في rdxمكان يتم وضعه بعد ذلك rbx(عنوان GOT المطلق). نتيجة لذلك ، نحصل على عنوان PLT المدخل global_funcفي rdx.



لاحظ أنه مرة أخرى يتم استخدام GOT كمرساة ، هذه المرة لتوفير مرجع مستقل عن العنوان لإزاحة إدخال PLT.



متوسط ​​نموذج رمز الموافقة المسبقة عن علم



أخيرًا ، سنقسم الشفرة التي تم إنشاؤها لنموذج PIC العادي:



int main(int argc, const char* argv[])
{
  15:   55                      push   %rbp
  16:   48 89 e5                mov    %rsp,%rbp
  19:   53                      push   %rbx
  1a:   48 83 ec 28             sub    $0x28,%rsp
  1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx
  25:   89 7d dc                mov    %edi,-0x24(%rbp)
  28:   48 89 75 d0             mov    %rsi,-0x30(%rbp)
    int t = global_func(argc);
  2c:   8b 45 dc                mov    -0x24(%rbp),%eax
  2f:   89 c7                   mov    %eax,%edi
  31:   b8 00 00 00 00          mov    $0x0,%eax
  36:   e8 00 00 00 00          callq  3b <main+0x26>
  3b:   89 45 ec                mov    %eax,-0x14(%rbp)
    t += global_arr[7];
  3e:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  45:   8b 40 1c                mov    0x1c(%rax),%eax
  48:   01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr[7];
  4b:   8b 05 00 00 00 00       mov    0x0(%rip),%eax
  51:   01 45 ec                add    %eax,-0x14(%rbp)
    t += global_arr_big[7];
  54:   48 8b 05 00 00 00 00    mov    0x0(%rip),%rax
  5b:   8b 40 1c                mov    0x1c(%rax),%eax
  5e:   01 45 ec                add    %eax,-0x14(%rbp)
    t += static_arr_big[7];
  61:   48 b8 00 00 00 00 00    movabs $0x0,%rax
  68:   00 00 00
  6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
  6f:   01 45 ec                add    %eax,-0x14(%rbp)
    return t;
  72:   8b 45 ec                mov    -0x14(%rbp),%eax
}
  75:   48 83 c4 28             add    $0x28,%rsp
  79:   5b                      pop    %rbx
  7a:   c9                      leaveq
  7b:   c3                      retq


الانتقال:



Relocation section '.rela.text' at offset 0x62d60 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000021  00160000001a R_X86_64_GOTPC32  0000000000000000 _GLOBAL_OFFSET_TABLE_ - 4
000000000037  001700000004 R_X86_64_PLT32    0000000000000000 global_func - 4
000000000041  001200000009 R_X86_64_GOTPCREL 0000000000000000 global_arr - 4
00000000004d  000300000002 R_X86_64_PC32     0000000000000000 .data + 1b8
000000000057  001300000009 R_X86_64_GOTPCREL 0000000000000000 global_arr_big - 4
000000000063  000a00000019 R_X86_64_GOTOFF64 0000000000030d40 static_arr_big + 0


أولاً ، دعنا نزيل استدعاء الوظيفة. على غرار النموذج الصغير ، في النموذج الأوسط ، نفترض أن مراجع الكود لا تتجاوز حدود إزاحة 32-بت RIP ، وبالتالي ، فإن الرمز المراد الاتصال به global_funcيشبه تمامًا نفس الرمز في نموذج PIC الصغير ، وكذلك في حالات صفيفات البيانات الصغيرة static_arrو global_arr. لذلك ، سنركز على صفائف البيانات الضخمة ، ولكن دعونا نتحدث أولاً عن المقدمة: هنا تختلف عن مقدمة نموذج البيانات الضخمة.



1e:   48 8d 1d 00 00 00 00    lea    0x0(%rip),%rbx


هذه هي المقدمة كلها: من أجل استخدام النقل R_X86_64_GOTPC32لإدخال عنوان GOT rbx، استغرق الأمر فريقًا واحدًا فقط (مقارنة بثلاثة في النموذج الكبير). ماهو الفرق؟ النقطة الأساسية هي أنه نظرًا لأن النموذج GOT ليس في النموذج الأوسط جزءًا من "أقسام البيانات الضخمة" ، فإننا نفترض توفره في نوبة 32 بت. في النموذج الكبير ، لم نتمكن من عمل مثل هذه الافتراضات ، وكان علينا استخدام التحول الكامل 64 بت.



من المثير للاهتمام حقيقة أن رمز الوصول global_arr_bigمشابه لنفس الرمز في نموذج PIC الصغير. هذا هو نفس السبب في أن مقدمة النموذج الأوسط أقصر من مقدمة النموذج الكبير: نحن نفترض أن GOT متاح في عنونة RIP ذات 32 بت. في الواقع ، لنفسيglobal_arr_bigمن المستحيل الحصول على مثل هذا الوصول ، ولكن هذه الحالة لا تزال تغطي GOT ، حيث أنها موجودة بالفعل global_arr_big، علاوة على ذلك ، في شكل عنوان 64 بت كامل.



لكن الوضع يختلف بالنسبة لـ static_arr_big:



  t += static_arr_big[7];
61:   48 b8 00 00 00 00 00    movabs $0x0,%rax
68:   00 00 00
6b:   8b 44 03 1c             mov    0x1c(%rbx,%rax,1),%eax
6f:   01 45 ec                add    %eax,-0x14(%rbp)


هذه الحالة شبيهة بنموذج رمز PIC الكبير ، حيث أننا ما زلنا نحصل على العنوان المطلق للرمز ، وهو ليس في GOT نفسه. نظرًا لأن هذا رمز كبير ، لا يمكن افتراضه في الجيجابايتين الأقل ، فإننا ، كما هو الحال في النموذج الكبير ، نحتاج إلى تحويل PIC 64 بت.



ملاحظات:



[1] هل كود عدم الخلط بين النماذج مع بيانات 64 بت نماذج و نماذج ذاكرة إنتل ، وهذه كلها موضوعات مختلفة.



[2] من المهم أن نتذكر: يتم إنشاء الأوامر الفعلية من قبل المترجم ، ويتم إصلاح أوضاع العنونة تمامًا في هذه الخطوة. لا يستطيع المترجم معرفة أي البرامج أو المكتبات المشتركة التي ستقع فيها وحدة الكائن ، قد يكون بعضها صغيرًا ، في حين أن البعض الآخر قد يكون كبيرًا. يعرف الرابط حجم البرنامج النهائي ، ولكن فات الأوان: لا يمكن للرابط إلا تصحيح تحول الأوامر مع إعادة التوطين ، ولا يغير الأوامر نفسها. وبالتالي ، فإن "اتفاق" نموذج الكود يجب أن "يوقع" من قبل المبرمج في مرحلة التجميع.



[3] إذا ظل شيء ما غير واضح ، فراجع المقالة التالية .



[4] ومع ذلك ، تزداد الأحجام تدريجيًا. في المرة الأخيرة التي راجعت فيها بناء Clang لـ Debug + Asserts ، وصل تقريبًا إلى غيغابايت واحد ، والذي بفضله الكثير من التعليمات البرمجية التي تم إنشاؤها تلقائيًا.



[5] إذا كنت لا تزال لا أعرف كيف يعمل PIC (في كل عام، وعلى وجه الخصوص لبنية x64) و، وحان الوقت لقراءة المواد التالية حول موضوع: واحد و اثنين .



[6] وبالتالي ، لا يمكن للرابط حل الروابط بمفرده ، وعليه تحويل معالجة GOT إلى المحمل الديناميكي.



[7] 0x25 - 0x7 + GOT - 0x27 + 0x9 = GOT









All Articles