فريقنا Immunant يحب Rust ويعمل بنشاط على C2Rust ، وهو إطار ترحيل يعتني بكامل روتين الترحيل إلى Rust. نحن نسعى جاهدين لإدخال تحسينات الأمان تلقائيًا في رمز Rust المحول ومساعدة المبرمج على القيام بذلك بنفسه عندما يفشل الإطار. ومع ذلك ، أولاً وقبل كل شيء ، نحن بحاجة إلى إنشاء مترجم موثوق يتيح للمستخدمين بدء استخدام Rust. أصبح اختبار برامج CLI الصغيرة قديمًا ببطء ، لذلك قررنا نقل Quake 3 إلى Rust ، وبعد بضعة أيام ، كنا على الأرجح أول من لعب Quake3 على Rust!
التحضير: زلزال 3 مصادر
بعد دراسة الكود المصدري لل Quake 3 الأصلي والشوك المختلفة ،
استقرنا على
ioquake3 . هذا شوكة تم إنشاؤها من قبل المجتمع في Quake 3 ، والتي لا تزال مدعومة ومبنية على منصات حديثة.
كنقطة انطلاق ، قررنا التأكد من أنه يمكننا تجميع المشروع في شكله الأصلي:
$ make release
عند إنشاء ioquake3 ، يتم إنشاء العديد من المكتبات والملفات القابلة للتنفيذ:
$ tree --prune -I missionpack -P "*.so|*x86_64" . └── build └── debug-linux-x86_64 ├── baseq3 │ ├── cgamex86_64.so
من بين هذه المكتبات ، يمكن تجميع مكتبات واجهة المستخدم والعميل والخوادم إما
كتجميع Quake VM أو كمكتبات مشتركة X86 أصلية. في مشروعنا ، قررنا استخدام الإصدارات الأصلية. ستكون ترجمة VMs إلى Rust واستخدام إصدارات QVM أبسط بكثير ، لكننا أردنا اختبار C2Rust بدقة.
في مشروع النقل الخاص بنا ، ركزنا على واجهة المستخدم ، اللعبة ، العميل ، عارض OpenGL1 ، والقابل للتنفيذ الرئيسي. يمكننا أيضًا ترجمة عارض OpenGL2 ، لكننا قررنا تخطي هذا لأنه يستخدم كمًا كبيرًا من
.glsl
التظليل
.glsl
، والتي يقوم نظام
.glsl
حرفية سلسلة في التعليمات البرمجية المصدر لـ C. بعد التجميع ، سنضيف دعمًا للبرامج النصية للبناء للتضمين كود GLSL في سلاسل الصدأ ، ولكن لا توجد حتى الآن طريقة تلقائية جيدة لنقل هذه الملفات المؤقتة التي تم إنشاؤها تلقائيًا. لذلك بدلاً من ذلك ، قمنا بترجمة مكتبة عارض OpenGL1 وأجبرنا اللعبة على استخدامها بدلاً من العارض الافتراضي. بالإضافة إلى ذلك ، قررنا تخطي الخادم المخصص وملفات المهام المعبأة ، لأنها لن تكون صعبة النقل ولن تكون ضرورية للتظاهرة الخاصة بنا.
انقل الزلزال 3
من أجل الحفاظ على بنية الدليل المستخدمة في Quake 3 وعدم تغيير التعليمات البرمجية المصدر ، نحتاج إلى الحصول على نفس الملفات الثنائية تمامًا كما هو الحال في التجميع الأصلي ، أي أربع مكتبات مشتركة وملف قابل للتنفيذ واحد.
نظرًا لأن C2Rust ينشئ ملفات تجميع Cargo ، فإن كل ثنائي يتطلب صندوق صدأ خاص به مع ملف
Cargo.toml
المقابل.
لكي يقوم C2Rust بإنشاء صندوق واحد لكل ملف ثنائي للمخرجات ، سوف يحتاج أيضًا إلى قائمة بالملفات الثنائية مع الكائن المصدر أو الملفات المصدر ، بالإضافة إلى مكالمة رابط تستخدم لإنشاء كل ملف ثنائي (تستخدم لتحديد تفاصيل أخرى ، على سبيل المثال ، تبعيات المكتبة).
ومع ذلك ، سرعان ما واجهنا قيودًا واحدة ناتجة عن الطريقة التي يعترض بها C2Rust عملية الإنشاء الأصلية: يتلقى C2Rust ملف
قاعدة بيانات تجميع عند الإدخال الذي يحتوي على قائمة بأوامر الترجمة التي يتم تنفيذها أثناء الإنشاء. ومع ذلك ، تحتوي قاعدة البيانات هذه على أوامر ترجمة
فقط دون استدعاءات رابط. تحتوي معظم الأدوات التي تنشئ قاعدة البيانات هذه على هذا القصد المتعمد ، على سبيل المثال ،
cmake
باستخدام
CMAKE_EXPORT_COMPILE_COMMANDS
،
bear
and
compiledb
. في تجربتنا ، فإن الأداة الوحيدة التي تتضمن أوامر
build-logger
هي سجل
build-logger
تم إنشاؤه بواسطة
CodeChecker
، والذي لم نستخدمه لأننا علمنا به فقط بعد كتابة الأغلفة الخاصة بنا (الموصوفة أدناه). هذا يعني أنه لتجميع برنامج C مع عدة ملفات ثنائية ، لا يمكننا استخدام ملف
compile_commands.json
تم إنشاؤه بواسطة أي من الأدوات الشائعة.
لذلك ، قمنا بكتابة البرامج النصية الخاصة ببرنامج التحويل البرمجي
compile_commands.json
الخاصة بنا والتي تقوم بتفريغ كافة المكالمات إلى برنامج التحويل البرمجي والرابط إلى قاعدة البيانات ، ثم تحويلها إلى
compile_commands.json
الموسعة. بدلاً من التجميع المعتاد باستخدام أمر مثل:
$ make release
أضفنا الأغلفة لاعتراض التجمع مع:
$ make release CC=/path/to/C2Rust/scripts/cc-wrappers/cc
تقوم Wrappers بإنشاء دليل للعديد من ملفات JSON ، واحد لكل مكالمة.
البرنامج النصي الثاني يجمع كل منهم في ملف
compile_commands.json
واحد جديد ، والذي يحتوي على كل من أوامر
compile_commands.json
والتجميع. ثم قمنا بتمديد C2Rust بحيث يقرأ أوامر الإنشاء من قاعدة البيانات ويقوم بإنشاء قفص منفصل لكل ثنائي مرتبط. بالإضافة إلى ذلك ، يقوم C2Rust الآن أيضًا بقراءة تبعيات المكتبة لكل ملف ثنائي ويضيفها تلقائيًا إلى ملف
build.rs
الخاص
build.rs
المقابل.
لتحسين الراحة ، يمكن جمع كل الثنائيات في وقت واحد عن طريق وضعها داخل
مساحة العمل . ينشئ
Cargo.toml
ملف
Cargo.toml
بمساحة العمل على المستوى
Cargo.toml
، حتى نتمكن من إنشاء المشروع باستخدام
cargo build
الوحيد في دليل
quake3-rs
:
$ tree -L 1 . ├── Cargo.lock ├── Cargo.toml ├── cgamex86_64 ├── ioquake3 ├── qagamex86_64 ├── renderer_opengl1_x86_64 ├── rust-toolchain └── uix86_64 $ cargo build --release
القضاء على خشونة
عندما حاولنا أولاً ترجمة الشفرة المترجمة ، واجهنا مشكلتين في مصادر Quake 3: كانت هناك حالات حدودية لم يتمكن C2Rust من التعامل معها (لا بشكل صحيح ولا على الإطلاق).
مؤشرات الصفيف
تحتوي العديد من الأماكن في التعليمات البرمجية المصدر الأصلي على التعبيرات التي تشير إلى العنصر التالي بعد عنصر الصفيف الأخير. فيما يلي مثال رمز C مبسط:
int array[1024]; int *p;
يسمح المعيار C (انظر ، على سبيل المثال ،
C11 ، القسم 6.5.6 ) للمؤشرات بعنصر يتجاوز نهاية المصفوفة. ومع ذلك ، يحظر Rust هذا ، حتى لو أخذنا عنوان العنصر فقط. وجدنا أمثلة لمثل هذا النمط في دالة
AAS_TraceClientBBox
.
أشار برنامج التحويل البرمجي Rust أيضًا إلى مثال مشابه ، ولكن في الواقع
G_TryPushingEntity
في
G_TryPushingEntity
، حيث يحتوي التعليمة الشرطية على النموذج ، وليس
>=
. ثم يتم إلغاء تحديد المؤشر الذي ينطلق من الحدود بعد الإنشاء الشرطي ، وهو خطأ أمان الذاكرة.
لتجنب هذه المشكلة في المستقبل ، قمنا بإصلاح محوّل C2Rust بحيث يستخدم حساب المؤشر لحساب عنوان عنصر الصفيف ، بدلاً من استخدام عملية فهرسة الصفيف. بفضل هذا الإصلاح ، تتم الآن ترجمة التعليمات البرمجية التي تستخدم "عنوان عنصر مماثل في نهاية المصفوفة" ونفذت بشكل صحيح دون تعديلات.
عناصر صفيف متغير الطول
أطلقنا اللعبة لاختبار كل شيء ، وفزعنا على الفور من Rust:
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', quake3-client/src/cm_polylib.rs:973:17
بإلقاء نظرة على
cm_polylib.c
، لاحظنا أنه
cm_polylib.c
الحقل
p
في الهيكل التالي:
typedef struct { int numpoints; vec3_t p[4];
يعد الحقل
p
في البنية إصدارًا لعضو الصفيف المرن الذي لا يدعمه معيار C99 ، ولكن لا يزال مقبولًا من قبل
gcc
. تتعرف C2Rust على عناصر المصفوفات ذات الطول المتغير باستخدام بناء الجملة C99 (
vec3_t p[]
) وتنفذ
مجريات الأمور البسيطة لتحديد إصدارات هذا النموذج قبل C99 (صفائف ذات أحجام 0 و 1 في نهاية الهياكل ؛ كما وجدنا العديد من الأمثلة في شفرة المصدر ioquake3).
تغيير الهيكل أعلاه إلى بناء جملة C99 قضى على الذعر:
typedef struct { int numpoints; vec3_t p[];
ستكون محاولة تصحيح هذا النمط تلقائيًا في الحالة العامة (بأحجام صفيف مختلفة عن 0 و 1) صعبة للغاية ، لأنه سيتعين علينا التمييز بين المصفوفات العادية وعناصر المصفوفات ذات الأحجام التعسفية المتغيرة الطول. لذلك ، بدلاً من ذلك ، نوصي بتصحيح رمز C الأصلي يدويًا ، كما فعلنا مع ioquake3.
تعادل يعمل في رمز المجمع المضمنة
مصدر آخر
/usr/include/bits/select.h
هو رمز المجمّع C- المجمّع من رأس النظام
/usr/include/bits/select.h
:
# define __FD_ZERO(fdsp) \ do { \ int __d0, __d1; \ __asm__ __volatile__ ("cld; rep; " __FD_ZERO_STOS \ : "=c" (__d0), "=D" (__d1) \ : "a" (0), "0" (sizeof (fd_set) \ / sizeof (__fd_mask)), \ "1" (&__FDS_BITS (fdsp)[0]) \ : "memory"); \ } while (0)
تحديد الإصدار الداخلي من الماكرو
__FD_ZERO
. يثير هذا التعريف حالة حدودية نادرة لـ
gcc
:
معاملات I / O مرتبطة بأحجام مختلفة. عامل التشغيل الإخراج
"=D" (__d1)
يربط سجل
__d1
بالمتغير
__d1
كقيمة 32 بت ، و
"1" (&__FDS_BITS (fdsp)[0])
يربط نفس السجل بالعنوان
fdsp->fds_bits
كمؤشر 64 بت.
gcc
clang
حل هذا التطابق. باستخدام سجل 64 بت
rdi
واقتطاع قيمته قبل تعيين القيمة
__d1
، ويستخدم Rust دلالات LLVM افتراضيًا ، حيث تظل هذه الحالة غير محددة. في تصميمات التصحيح (وليس في الإصدارات التي تصرفت بشكل جيد) ، رأينا أنه يمكن تعيين كلا
edi
سجل
edi
، بسببه يتم اقتطاع المؤشر إلى 32 بت قبل رمز المجمّع المدمج ، والذي يسبب الفشل.
نظرًا لأن
rustc
يمرر كود مجمّع Rust المدمج إلى LLVM دون تغيير يذكر ، فقد قررنا إصلاح هذه الحالة المعينة في C2Rust. قمنا بتطبيق
c2rust-asm-casts
، والتي تعمل على حل هذه المشكلة بفضل نظام الصدأ باستخدام وظائف
c2rust-asm-casts
التي تقوم تلقائيًا بتوسيع وربط المعاملات المرتبطة بحجم داخلي كبير بما يكفي لاحتواء كلتا المعاملتين. يترجم الرمز أعلاه بشكل صحيح إلى ما يلي:
let mut __d0: c_int = 0; let mut __d1: c_int = 0;
تجدر الإشارة إلى أن هذا الرمز لا يتطلب أي أنواع لقيم المدخلات والمخرجات في تجميع رمز المجمّع ؛ عند حل تعارضات الكتابة ، تعتمد بدلاً من ذلك عليها في إخراج أنواع الصدأ (أنواع
fresh6
و
fresh8
).
محاذاة المتغيرات العالمية
المصدر الأخير للفشل هو المتغير الشامل التالي الذي يقوم بتخزين ثابت SSE:
static unsigned char ssemask[16] __attribute__((aligned(16))) = { "\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\xFF\x00\x00\x00\x00" };
يدعم Rust حاليًا سمة المحاذاة للأنواع الهيكلية ، ولكن ليس للمتغيرات العالمية ، أي عناصر
static
. نظرنا في طرق لحل هذه المشكلة في الحالة العامة ، إما في Rust أو في C2Rust ، لكن في الوقت الحالي في ioquake3 قررنا إصلاحها يدويًا باستخدام ملف
تصحيح قصير. يستبدل ملف التصحيح هذا المكافئ
ssemask
يلي:
#[repr(C, align(16))] struct SseMask([u8; 16]); static mut ssemask: SseMask = SseMask([ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 0, 0, 0, ]);
تشغيل quake3- روبية
عند
cargo build --release
يتم إنشاء الثنائيات ، ولكن يتم إنشاؤها ضمن
target/release
بهيكل دليل لا يتعرف عليه
ioquake3
الثنائية. لقد كتبنا
نصًا ينشئ روابط رمزية في الدليل الحالي لإعادة إنشاء بنية الدليل الصحيحة (بما في ذلك الروابط إلى ملفات
.pk3
التي تحتوي على موارد اللعبة):
$ /path/to/make_quake3_rs_links.sh /path/to/quake3-rs/target/release /path/to/paks
يجب أن يشير المسار
/path/to/paks
إلى الدليل الذي يحتوي على ملفات
.pk3
.
الآن لنقم بتشغيل اللعبة! نحتاج إلى اجتياز
+set vm_game 0
، إلخ ، لذلك نقوم بتحميل هذه الوحدات كمكتبات مشتركة Rust ، وليس كتجميع QVM ، وكذلك
cl_renderer
لاستخدام عارض OpenGL1.
$ ./ioquake3 +set sv_pure 0 +set vm_game 0 +set vm_cgame 0 +set vm_ui 0 +set cl_renderer "opengl1"
و ...
أطلقنا Quake3 على الصدأ!
إليك مقطع فيديو حول كيفية تبديل Quake 3 وتنزيل اللعبة ولعبها قليلاً:
يمكنك دراسة
المصادر المنقولة في الفرع
transpiled
. يوجد أيضًا فرع
refactored
يحتوي على نفس
المصادر مع العديد من
أوامر إعادة التسكين المطبقة مسبقًا.
كيفية تبديل
إذا كنت ترغب في محاولة نقل Quake 3 وتشغيلها بنفسك ، فضع في اعتبارك أنك ستحتاج إلى موارد لعبة Quake 3 الخاصة بك أو موارد تجريبية من الإنترنت. ستحتاج أيضًا إلى تثبيت C2Rust (في وقت كتابة هذا التقرير ، الإصدار الليلي المطلوب هو
nightly-2019-12-05
، لكننا نوصيك بالبحث في
مستودع C2Rust أو في
صناديق. للعثور على أحدث إصدار):
$ cargo +nightly-2019-12-05 install c2rust
ونسخ من مستودعات C2Rust و ioquake3 الخاصة بنا:
$ git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcbbb5a89cbbb5a8b4a9bef2bfb3b1">[email protected]</a>:immunant/c2rust.git $ git clone <a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="dcbbb5a89cbbb5a8b4a9bef2bfb3b1">[email protected]</a>:immunant/ioq3.git
كبديل لتثبيت
c2rust
باستخدام الأمر أعلاه ، يمكنك إنشاء C2Rust يدويًا باستخدام
cargo build --release
. في أي حال ، لا تزال هناك حاجة إلى مستودع C2Rust ، لأنه يحتوي على البرامج النصية لبرنامج تجميع المحول البرمجي المطلوبة لنقل ioquake3.
لقد قمنا بنشر برنامج
نصي ينقل رمز C تلقائيًا ويطبق تصحيح
ssemask
. لاستخدامها ، قم بتشغيل الأمر التالي من المستوى العلوي لمستودع
ioq3
:
$ ./transpile.sh </path/to/C2Rust repository> </path/to/c2rust binary>
يجب أن يقوم هذا الأمر بإنشاء دليل
quake3-rs
فرعي يحتوي على كود Rust ، والذي يمكنك من خلاله تنفيذ
cargo build --release
والخطوات المتبقية الموضحة أعلاه.