Rust: يمكن أن يؤدي تحسين حلقة LLVM إلى تعطل البرامج الآمنة

تم إنشاؤها على ٢٩ سبتمبر ٢٠١٥  ·  97تعليقات  ·  مصدر: rust-lang/rust

يتعطل المقتطف التالي عند تجميعه في وضع الإصدار في الوضع الحالي الثابت والتجريبي والليلي:

enum Null {}

fn foo() -> Null { loop { } }

fn create_null() -> Null {
    let n = foo();

    let mut i = 0;
    while i < 100 { i += 1; }
    return n;
}

fn use_null(n: Null) -> ! {
    match n { }
}


fn main() {
    use_null(create_null());
}

https://play.rust-lang.org/؟gist=1f99432e4f2dccdf7d7e&version=stable

يعتمد هذا على المثال التالي من LLVM الذي يزيل حلقة علمت بها: https://github.com/simnalamburt/snippets/blob/12e73f45f3/rust/infinite.rs.
ما يبدو أنه يحدث هو أنه نظرًا لأن C تسمح لـ LLVM بإزالة الحلقات اللانهائية التي ليس لها أي آثار جانبية ، فقد انتهى بنا الأمر إلى تنفيذ match يجب أن يتم تسويته.

A-LLVM C-bug E-medium I-needs-decision I-unsound 💥 P-medium T-compiler WG-embedded

التعليق الأكثر فائدة

في حالة رغبة أي شخص في لعب لعبة غولف كود الحالة التجريبية:

pub fn main() {
   (|| loop {})()
}

مع العلم -Z insert-sideeffect rustc ، الذي تمت إضافته بواسطة sfanxiang في https://github.com/rust-lang/rust/pull/59546 ، يستمر في التكرار :)

قبل:

main:
  ud2

بعد:

main:
.LBB0_1:
  jmp .LBB0_1

ال 97 كومينتر

LLVM IR الخاص بالشفرة المحسنة هو

; Function Attrs: noreturn nounwind readnone uwtable
define internal void @_ZN4main20h5ec738167109b800UaaE() unnamed_addr #0 {
entry-block:
  unreachable
}

يكسر هذا النوع من التحسين الافتراض الرئيسي الذي يجب أن يصمد عادة على الأنواع غير المأهولة: يجب أن يكون من المستحيل الحصول على قيمة من هذا النوع.
يقترح rust-lang / rfcs # 1216 التعامل بوضوح مع مثل هذه الأنواع في Rust. قد يكون فعالاً في ضمان عدم اضطرار LLVM مطلقًا إلى التعامل معها وفي إدخال الكود المناسب لضمان الاختلاف عند الحاجة (IIUIC يمكن تحقيق ذلك بسمات مناسبة أو مكالمات داخلية).
تمت مناقشة هذا الموضوع مؤخرًا في القائمة البريدية لـ LLVM: http://lists.llvm.org/pipermail/llvm-dev/2015- July/088095.html

الفرز: أنا مرشح

يبدو سيئا! إذا لم يكن لدى LLVM طريقة لقول "نعم ، هذه الحلقة لا نهائية حقًا" على الرغم من أنه قد نضطر فقط للجلوس والانتظار حتى تسوية المناقشة الأولية.

هناك طريقة لمنع الحلقات اللانهائية من التحسين وهي إضافة unsafe {asm!("" :::: "volatile")} بداخلها. هذا مشابه لما هو مضمن llvm.noop.sideeffect الذي تم اقتراحه في القائمة البريدية لـ LLVM ، ولكنه قد يمنع بعض التحسينات.
من أجل تجنب فقدان الأداء والاستمرار في ضمان عدم تحسين الوظائف / الحلقات المتباينة بعيدًا ، أعتقد أنه يجب أن يكون كافياً لإدخال حلقة فارغة غير قابلة للتحسين (مثل loop { unsafe { asm!("" :::: "volatile") } } ) إذا كانت القيم غير المأهولة في نطاق.
إذا قام LLVM بتحسين الكود الذي يجب أن يتباعد لدرجة أنه لم يعد يتباعد ، فإن هذه الحلقات ستضمن أن تدفق التحكم لا يزال غير قادر على المتابعة.
في حالة "الحظ" التي يتعذر فيها على LLVM تحسين الشفرة المتباعدة ، ستتم إزالة هذه الحلقة بواسطة DCE.

هل هذا متعلق بـ # 18785؟ هذا الأمر يتعلق بالعودة اللانهائية لتكون UB ، ولكن يبدو أن السبب الأساسي قد يكون مشابهًا: لا يعتبر LLVM عدم التوقف من الآثار الجانبية ، لذلك إذا لم يكن للوظيفة أي آثار جانبية سوى عدم التوقف ، يسعدها تحسينها انها بعيدا.

تضمين التغريدة

إنها نفس المشكلة.

نعم ، يبدو أنه نفس الشيء. علاوة على ذلك ، يوضحون كيفية الحصول على undef ، والذي أفترض أنه ليس من الصعب القيام بتعطل برنامج (يبدو آمنًا).

: +1:

تحطم ، أو ربما أسوأ من ذلك https://play.rust-lang.org/؟gist=15a325a795244192bdce&version=stable

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

  1. ستنتهي الحلقة OR
  2. سيكون للحلقة آثار جانبية (عمليات الإدخال / الإخراج وما إلى ذلك ، أنسى بالضبط كيف يتم تعريف ذلك في C)

يجب أن يكون أي من هذا كافيًا لتجنب السلوك غير المحدد.

الفرز: ف متوسط

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

نقلاً عن مناقشة القائمة البريدية لـ LLVM:

 The implementation may assume that any thread will eventually do one of the following:
   - terminate
   - make a call to a library I/O function
   - access or modify a volatile object, or
   - perform a synchronization operation or an atomic operation

 [Note: This is intended to allow compiler transformations such as removal of empty loops, even
  when termination cannot be proven. — end note ]

dotdash المقتطف الذي

فيما يتعلق بالسلوك المتوقع لـ LLVM IR ، هناك بعض الالتباس. https://llvm.org/bugs/show_bug.cgi؟id=24078 يظهر أنه لا يبدو أن هناك مواصفات دقيقة وصريحة لدلالات الحلقات اللانهائية في LLVM IR. يتوافق مع دلالات C ++ ، على الأرجح لأسباب تاريخية وللتيسير (لقد تمكنت فقط من تعقب https://groups.google.com/forum/#!topic/llvm-dev/j2vlIECKkdE الذي يشير على ما يبدو إلى وقت عندما لا يتم تحسين الحلقات اللانهائية بعيدًا ، قبل تحديث مواصفات C / C ++ لبعض الوقت للسماح بذلك).

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

@ ranma42 أنا على دراية بذلك ، لقد اقتبست ذلك للتو كمرجع ، لأن أحد الاحتمالات للتغلب على هذا هو اكتشاف مثل هذه الحلقات في الصدأ وإضافة واحدة مما سبق إليه لمنع LLVM من إجراء هذا التحسين.

هل هذه قضية سلامة؟ إذا كان الأمر كذلك ، يجب أن نضع علامة عليه على هذا النحو.

نعم ، باتباع مثال @ ranma42 ، توضح هذه الطريقة كيف رابط الملعب

تضمين التغريدة

تتمثل السياسة في أنه يجب وضع علامة على مشكلات التعليمات البرمجية الخاطئة التي تمثل أيضًا مشكلات تتعلق بالسلامة (أي معظمها) I-wrong .

لذلك فقط لتلخيص المناقشة السابقة ، يوجد حقًا خياران هنا يمكنني رؤيته:

  • انتظر LLVM لتقديم حل.
  • قم بتقديم عبارات no-op asm أينما كان هناك حلقة لا نهائية أو تكرار لا نهائي (# 18785).

هذا الأخير سيء نوعًا ما لأنه يمكن أن يمنع التحسين ، لذلك نرغب في القيام بذلك باعتدال - في الأساس في أي مكان لا يمكننا فيه إثبات الإنهاء بأنفسنا يمكنك أيضًا تصوير ربطه أكثر قليلاً بكيفية تحسين LLVM - على سبيل المثال ، التقديم فقط إذا تمكنا من اكتشاف سيناريو قد يعتبره LLVM حلقة / عودية لا نهائية - ولكن هذا سيتطلب (أ) تتبع LLVM و (ب ) تتطلب معرفة أعمق مما أملك ، على الأقل.

انتظر LLVM لتقديم حل.

ما هو خطأ LLVM الذي يتتبع هذه المشكلة؟

ملاحظة جانبية: while true {} يعرض هذا السلوك . ربما يجب ترقية النسالة إلى خطأ افتراضيًا والحصول على ملاحظة تفيد بأن هذا حاليًا يمكن أن يُظهر سلوكًا غير محدد؟

لاحظ أيضًا أن هذا غير صالح لـ C. LLVM مما يجعل هذه الوسيطة يعني وجود خطأ في clang.

void foo() { while (1) { } }

void create_null() {
        foo();

        int i = 0;
        while (i < 100) { i += 1; }
}

__attribute__((noreturn))
void use_null() {
        __builtin_unreachable();
}


int main() {
        create_null();
        use_null();
}

هذا يتعطل مع التحسينات. هذا سلوك غير صالح بموجب معيار C11:

An iteration statement whose controlling expression is not a constant
expression, [note 156] that performs no  input/output  operations,
does  not  access  volatile  objects,  and  performs  no synchronization or
atomic operations in its body, controlling expression, or (in the case of
a for statement) its expression-3, may be   assumed   by   the
implementation to terminate. [note 157]

156: An omitted controlling expression is replaced by a nonzero constant,
     which is a constant expression.
157: This  is  intended  to  allow  compiler  transformations  such  as
     removal  of  empty  loops  even  when termination cannot be proven. 

لاحظ أن "تعبيره المسيطر ليس تعبيرًا ثابتًا" - while (1) { } ، 1 هو تعبير ثابت ، وبالتالي قد لا تتم إزالته .

هل إزالة الحلقة عبارة عن تمريرة تحسين يمكننا إزالتها ببساطة؟

ubsan

هل عثرت على تقرير خطأ لذلك في Bugzilla لـ LLVM أو تم ملؤه؟ يبدو أنه في C ++ الحلقات اللانهائية التي _لا يمكن_ إنهاءها أبدًا هي سلوك غير محدد ، لكنها في لغة C هي سلوك محدد (إما يمكن إزالتها بأمان في بعض الحالات ، أو لا يمكنها في حالات أخرى).

أكرر نفسي من الرقم 42009: يمكن لهذا الخطأ ، في بعض الظروف ، أن يتسبب في انبعاث وظيفة قابلة للاستدعاء خارجيًا لا تحتوي على تعليمات الجهاز على الإطلاق. هذا لا ينبغي أن يحدث أبدا. إذا استنتج LLVM أنه لا يمكن استدعاء pub fn أبدًا عن طريق الكود الصحيح ، فيجب أن يصدر على الأقل تعليمة ملائمة كجسم لهذه الوظيفة.

تم طرح هذا في منشور المدونة هذا اليوم: https://blog.rom1v.com/2017/09/gnirehtet-rewritten-in-rust/

مع إعادة إنتاج أبسط: https://play.rust-lang.org/؟gist=e622f8a672fbc57ecc63eb4450d2fc0a&version=stable

خطأ LLVM لهذا هو https://bugs.llvm.org/show_bug.cgi؟id=965 (تم فتحه في عام 2006).

zackw LLVM لديها علامة لذلك: TrapUnreachable . لم أختبر هذا ، ولكن يبدو أن إضافة Options.TrapUnreachable = true; إلى LLVMRustCreateTargetMachine يجب أن يعالج مشكلتك. من المحتمل أن يكون لهذا تكلفة منخفضة بما يكفي بحيث يمكن إجراؤه بشكل افتراضي ، على الرغم من أنني لم أقم بإجراء أي قياسات.

@ oli-obk إنه للأسف ليس مجرد تمريرة لحذف الحلقة. تنشأ المشكلة من الافتراضات العامة ، على سبيل المثال: (أ) الفروع ليس لها آثار جانبية ، (ب) الوظائف التي لا تحتوي على تعليمات ذات آثار جانبية ليس لها آثار جانبية ، و (ج) استدعاءات وظائف ليس لها آثار جانبية يمكن نقلها أو تم الحذف.

يبدو أن هناك تصحيحًا: https://reviews.llvm.org/D38336

sunfishcode ، يبدو أن تصحيح LLVM الموجود على https://reviews.llvm.org/D38336 تم "قبوله" في 3 أكتوبر ، هل يمكنك تقديم تحديث لما يعنيه ذلك فيما يتعلق بعملية إصدار LLVM؟ ما هي الخطوة التالية بعد القبول ، وهل لديك فكرة عن إصدار LLVM المستقبلي الذي سيحتوي على هذا التصحيح؟

لقد تحدثت مع بعض الأشخاص في وضع عدم الاتصال الذين اقترحوا أن يكون لدينا موضوع llvmdev. الخيط هنا:

http://lists.llvm.org/pipermail/llvm-dev/2017- أكتوبر/118558.html

انتهى الآن ، وكانت النتيجة أنني بحاجة إلى إجراء تغييرات إضافية. أعتقد أن التغييرات ستكون جيدة ، على الرغم من أنها ستستغرق بعض الوقت للقيام بها.

شكرا على التحديث ، وشكرا جزيلا على جهودك!

لاحظ أن https://reviews.llvm.org/rL317729 هبطت في LLVM. تم التخطيط لهذا التصحيح ليكون له تصحيح متابعة مما يجعل الحلقات اللانهائية تظهر سلوكًا محددًا بشكل افتراضي ، لذلك كل ما نحتاج إلى AFAICT هو الانتظار وفي النهاية سيتم حل هذا الأمر بالنسبة لنا.

zackw لقد أنشأت الآن # 45920 لإصلاح مشكلة الوظائف التي لا تحتوي على كود.

bstrie نعم ، تم تحديد الخطوة الأولى ، وأنا أعمل على الخطوة الثانية لجعل LLVM يعطي سلوكًا محددًا لحلقات لانهائية بشكل افتراضي. إنه تغيير معقد ، ولا أعرف حتى الآن كم من الوقت سيستغرق للانتهاء ، لكنني سأقوم بنشر التحديثات هنا.

لم أتمكن من إعادة عرض هذا الآن: https://play.rust-lang.org/؟gist=529bd5ab326f7b627e559f64d514312f&version=stable

تضمين التغريدة هل حددت وضع الإصدار؟

@ kennytm Woops ، لا تهتم.

لاحظ أن https://reviews.llvm.org/rL317729 هبطت في LLVM. تم التخطيط لهذا التصحيح ليكون له تصحيح متابعة مما يجعل الحلقات اللانهائية تظهر سلوكًا محددًا بشكل افتراضي ، لذلك كل ما نحتاج إلى AFAICT هو الانتظار وفي النهاية سيتم حل هذا الأمر بالنسبة لنا.

لقد مرت عدة أشهر منذ هذا التعليق. هل يعلم أي شخص ما إذا كانت رقعة المتابعة حدثت أم أنها ستستمر؟

بدلاً من ذلك ، يبدو أن الجوهر llvm.sideeffect موجود في إصدار LLVM الذي نستخدمه: هل يمكننا إصلاح هذا بأنفسنا من خلال ترجمة حلقات Rust اللانهائية إلى حلقات LLVM التي تحتوي على التأثير الجانبي الجوهري؟

كما رأينا هو https://github.com/rust-lang/rust/issues/38136 و https://github.com/rust-lang/rust/issues/54214 ، هذا سيء بشكل خاص مع panic_implementation ، كتطبيق منطقي له سيكون loop {} ، وهذا سيجعل جميع التكرارات لـ panic! UB بدون أي كود unsafe . الذي ... ربما يكون أسوأ ما يمكن أن يحدث.

لقد صادفت هذه المشكلة في ضوء آخر. هذا مثال:

pub struct Container<'f> {
    string: &'f str,
    num: usize,
}

impl<'f> From<&'f str> for Container<'f> {
    #[inline(always)]
    fn from(string: &'f str) -> Container<'f> {
        Container::from(string)
    }
}

fn main() {
    let x = Container::from("hello");
    println!("{} {}", x.string, x.num);

    let y = Container::from("hi");
    println!("{} {}", y.string, y.num);

    let z = Container::from("hello");
    println!("{} {}", z.string, z.num);
}

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

SergioBenitez هذا البرنامج لا مثال على الحد الأدنى من العمل .

في إصدارات الإصدارات ، يمكن أن تفترض LLVM أنه ليس لديك عودية لانهائية ، وتحسن ذلك بعيدًا ( https://stackoverflow.com/a/5905171/1422197

gnzlbg آسف ، لكنك غير صحيح.

برنامج segfaults في وضع الإصدار. هذا هو بيت القصيد. أن التحسين يؤدي إلى سلوك غير سليم - أن دلالات LLVM و Rust ليست متفقة هنا - أنه يمكنني كتابة وتجميع برنامج Rust الآمن مع الصدأ الذي يسمح لي باستخدام ذاكرة غير مهيأة ، وفحص الذاكرة التعسفية ، والتوزيع التعسفي بين الأنواع ، منتهكًا دلالات اللغة. هذه هي نفس النقطة التي يتم توضيحها في هذا الموضوع. لاحظ أن البرنامج الأصلي أيضًا لا يخطئ في وضع التصحيح.

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

gnzlbg حسنًا ، تغييرًا طفيفًا في mwe من التحسين بعيدًا عن العودية اللانهائية ( هنا ) ، فإنه يولد قيمة غير مهيأة قدرها NonZeroUsize (والتي تبين أنها… 0 ، وبالتالي قيمة غير صالحة).

وهذا ما فعله SergioBenitez أيضًا

هل نحن متفقون على أن برنامج

إذا كان الأمر كذلك ، لا يمكنني العثور على أي loop s في مثال SergioBenitez ، لذلك لا أعرف كيف ستنطبق هذه المشكلة عليها (هذه المشكلة loop s اللانهائي بعد كل شيء). إذا كنت مخطئًا ، فيرجى توجيهي إلى loop في مثالك.

كما ذكرنا سابقًا ، تفترض LLVM أن العودية اللانهائية لا يمكن أن تحدث (تفترض أن جميع سلاسل العمليات تنتهي في النهاية) ، ولكن هذه ستكون مشكلة مختلفة عن هذه المشكلة.

لم أقم بفحص التحسينات التي يقوم بها LLVM أو الكود الذي تم إنشاؤه لأي من البرنامجين ، لكن لاحظ أن segfault ليس غير سليم ، إذا كان segfault هو كل ما يحدث. على وجه الخصوص ، الفائض المكدس الذي تم اكتشافه (عن طريق فحص المكدس + صفحة حماية غير معيّنة بعد نهاية المكدس) ولا تتسبب في أي مشاكل تتعلق بسلامة الذاكرة تظهر أيضًا على أنها segfaults. بالطبع ، يمكن أن تشير segfaults

rkruppe برنامج segfaults

gnzlbg البرنامج _لا _ لا يتكدس الفائض في وضع التحرير. في وضع الإصدار ، لا يقوم البرنامج بإجراء مكالمات دالة ؛ يتم دفع المكدس إلى عدد محدود من المرات ، فقط لتخصيص السكان المحليين.

لا يقوم البرنامج بتكديس الفائض في وضع التحرير.

وبالتالي؟ الشيء الوحيد المهم هو أن البرنامج النموذجي ، والذي هو أساسًا fn foo() { foo() } ، يحتوي على عودية لانهائية ، وهو ما لا يسمح به LLVM.

الشيء الوحيد المهم هو أن البرنامج النموذجي ، والذي هو أساسًا fn foo () {foo ()} ، له عودية لانهائية ، وهو ما لا يسمح به LLVM.

لا أعرف لماذا تقول هذا وكأنه يحل أي شيء. تفكر LLVM في التكرار اللانهائي وحلقات UB والتحسين وفقًا لذلك ، ومع ذلك فهي آمنة في Rust ، هي النقطة الكاملة لهذه المشكلة برمتها!

مؤلف https://reviews.llvm.org/rL317729 هنا ، يؤكد أنني لم أقم بتطبيق تصحيح المتابعة بعد.

يمكنك إدخال مكالمات @llvm.sideeffect اليوم لضمان عدم تحسين الحلقات. قد يؤدي ذلك إلى تعطيل بعض التحسينات ، ولكن من الناحية النظرية ليس كثيرًا ، حيث تم تعليم التحسينات الرئيسية كيفية فهمها. إذا وضع أحدهم مكالمات @llvm.sideeffect في جميع الحلقات أو الأشياء التي قد تتحول إلى حلقات (عودية ، فك ، inline asm ، أخرى؟) ، فهذا يكفي نظريًا لإصلاح المشكلة هنا.

من الواضح أنه سيكون من الأفضل وضع التصحيح الثاني في مكانه ، بحيث لا يكون من الضروري القيام بذلك. لا أعرف متى سأعود إلى تطبيق ذلك.

هناك بعض الاختلاف البسيط ، لكنني لست متأكدًا مما إذا كان ماديًا أم لا.

العودية

#[allow(unconditional_recursion)]
#[inline(never)]
pub fn via_recursion<T>() -> T {
    via_recursion()
}

fn main() {
    let a: String = via_recursion();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 personality i32 (i32, i32, i64, %"unwind::libunwind::_Unwind_Exception"*, %"unwind::libunwind::_Unwind_Context"*)* <strong i="9">@rust_eh_personality</strong> {
_ZN4core3ptr13drop_in_place17h95538e539a6968d0E.exit:
  ret void
}

عقدة

#[inline(never)]
pub fn via_loop<T>() -> T {
    loop {}
}

fn main() {
    let b: String = via_loop();
}
define internal void @_ZN10playground4main17h1daf53946e45b822E() unnamed_addr #2 {
start:
  unreachable
}

ميتا

الصدأ 1.29.1 ، تجميع في وضع التحرير ، عرض LLVM IR.

لا أعتقد أنه يمكننا ، بشكل عام ، اكتشاف العودية (كائنات السمات ، C FFI ، وما إلى ذلك) ، لذلك يتعين علينا استخدام llvm.sideeffect في كل موقع اتصال تقريبًا ما لم نتمكن من إثبات أن المكالمة الموقع لن يتكرر. يتطلب إثبات عدم وجود عمليات تكرار للحالات التي يمكن إثباتها فيها تحليل ما بين الإجراءات باستثناء أكثر البرامج تافهة مثل fn main() { main() } . قد يكون من الجيد معرفة تأثير تنفيذ هذا الإصلاح ، وما إذا كانت هناك حلول بديلة لهذه المشكلة.

gnzlbg هذا صحيح ، على الرغم من أنه يمكنك وضع @ llvm.sideeffects في مدخلات الوظائف ، بدلاً من مواقع الاتصال.

الغريب ، لا يمكنني إعادة إنتاج SEGFAULT في حالة اختبار SergioBenitez محليًا.

أيضًا ، بالنسبة إلى تجاوز سعة المكدس ، ألا يجب أن تكون هناك رسالة خطأ مختلفة؟ اعتقدت أن لدينا بعض التعليمات البرمجية لطباعة "فاض المكدس" أو نحو ذلك؟

RalfJung هل حاولت في وضع التصحيح؟ (يمكنني إعادة إنتاج تجاوز المكدس بشكل موثوق في وضع التصحيح في أجهزتي والملعب ، لذلك ربما تحتاج إلى ملء خطأ إذا لم يكن هذا هو الحال بالنسبة لك). في --release لن تحصل على تجاوز مكدس لأن كل هذه الشفرة تم تحسينها بشكل خاطئ.


تضمين التغريدة

هذا صحيح ، على الرغم من أنه يمكنك وضع @ llvm.sideeffects في إدخالات الوظائف ، بدلاً من مواقع الاتصال.

من الصعب تحديد أفضل طريقة للمضي قدمًا دون معرفة التحسينات التي يمنعها llvm.sideeffects بالضبط. هل يستحق الأمر محاولة توليد أقل عدد ممكن من @llvm.sideeffects ؟ إذا لم يكن الأمر كذلك ، فقد يكون وضعه في كل استدعاء وظيفي هو الشيء الأكثر مباشرة الذي يجب القيام به. بخلاف ذلك ، فإن IIUC ، ما إذا كانت هناك حاجة إلى @llvm.sideeffect يعتمد على ما يفعله موقع الاتصال:

trait Foo {
    fn foo(&self) { self.bar() }
    fn bar(&self);
}

struct A;
impl Foo for A {
    fn bar(&self) {} // not recursive
}
struct B;
impl Foo for B {
    fn bar(&self) { self.foo() } // recursive
}

fn main() {
    let a = A;
    a.bar(); // Ok - no @llvm.sideeffect needed anywhere
    let b = B;
    b.bar(); // We need @llvm.sideeffect on this call site
    let c: &[&dyn Foo] = &[&a, &b];
    for i in c {
        i.bar(); // We need @lvm.sideeffect here too
    }
}

AFAICT ، يتعين علينا وضع @llvm.sideeffect داخل الوظائف لمنع إزالتها بعيدًا ، لذلك حتى لو كانت هذه "التحسينات" تستحق العناء ، لا أعتقد أن التعامل معها مباشر مع النموذج الحالي. حتى لو كانت كذلك ، فإن هذه التحسينات ستعتمد على القدرة على إثبات عدم وجود تكرار.

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

بالتأكيد ، ولكن في وضع التصحيح ، لا تقوم LLVM بإجراء تحسينات الحلقة ، لذا لا توجد مشكلة.

إذا كان مكدس البرنامج يتدفق في وضع التصحيح ، فلا ينبغي أن يمنح ذلك ترخيص LLVM لإنشاء UB. تكمن المشكلة في معرفة ما إذا كان البرنامج النهائي يحتوي على UB ، ومن التحديق في IR لا يمكنني معرفة ذلك. إنها segfaults لكني لا أعرف لماذا. ولكن يبدو لي أن "تحسين" برنامج Stackoverflowing إلى برنامج يقوم على segfaults.

إذا كان مكدس البرنامج يتدفق في وضع التصحيح ، فلا ينبغي أن يمنح ذلك ترخيص LLVM لإنشاء UB.

ولكن يبدو لي أن "تحسين" برنامج Stackoverflowing إلى برنامج يقوم على segfaults.

في لغة C ، يُفترض أن خيط التنفيذ سينتهي أو ينفذ عمليات الوصول إلى الذاكرة المتطايرة أو الإدخال / الإخراج أو عملية ذرية متزامنة. سيكون من المفاجئ بالنسبة لي إذا لم يتطور LLVM-IR ليكون له نفس الدلالات إما عن طريق الصدفة أو عن طريق التصميم.

يحتوي رمز Rust على سلسلة تنفيذ لا تنتهي أبدًا ، ولا تؤدي أيًا من العمليات المطلوبة حتى لا تكون UB في C. أظن أننا ننتج نفس LLVM-IR الذي قد يقوم به برنامج C بسلوك غير محدد ، لذلك لا أعتقد أنه من المدهش أن LLVM يسيء تحسين برنامج Rust هذا.

إنها segfaults لكني لا أعرف لماذا.

يزيل LLVM العودية اللانهائية ، لذلك كما ذكر SergioBenitez أعلاه ، ينتقل البرنامج بعد ذلك إلى:

تم السماح بإنشاء إشارة إلى موقع ذاكرة عشوائية ، وتمت قراءة المرجع لاحقًا.

جزء البرنامج الذي يقوم بذلك هو هذا:

let x = Container::from("hello");  // invalid reference created here
println!("{} {}", x.string, x.num);  // invalid reference dereferenced here

حيث يبدأ Container::from عملية عودية لانهائية ، يستنتج LLVM أنه لا يمكن أن يحدث أبدًا ، ويستبدل ببعض القيمة العشوائية ، والتي يتم إلغاء الإشارة إليها بعد ذلك. يمكنك أن ترى إحدى الطرق العديدة التي يتم من خلالها إساءة استخدام هذا الأمر هنا: https://rust.godbolt.org/z/P7Snex On the playground (https://play.rust-lang.org/؟gist=f00d41cc189f9f6897d429350f3781ec&version=stable&mode = release & edition = 2015) يشعر المرء بذعر مختلف في الإصدار من تصميمات تصحيح الأخطاء بسبب هذا التحسين ، لكن UB هو UB هو UB.

يحتوي رمز Rust على سلسلة تنفيذ لا تنتهي أبدًا ، ولا تؤدي أيًا من العمليات المطلوبة حتى لا تكون UB في C. أظن أننا ننتج نفس LLVM-IR الذي قد يقوم به برنامج C بسلوك غير محدد ، لذلك لا أعتقد أنه من المدهش أن LLVM يسيء تحسين برنامج Rust هذا.

كان لدي انطباع بأنك جادلت أعلاه بأن هذا ليس نفس الخطأ مثل مشكلة الحلقة اللانهائية. يبدو أنني أخطأت في قراءة رسائلك بعد ذلك. اسف لخلط الامور.

لذا ، يبدو أن الخطوة التالية الجيدة هي رش بعض llvm.sideffect في IR الذي أنشأناه ، والقيام ببعض المعايير؟

في لغة C ، يُفترض أن خيط التنفيذ سينتهي أو ينفذ عمليات الوصول إلى الذاكرة المتطايرة أو الإدخال / الإخراج أو عملية ذرية متزامنة.

راجع للشغل، وهذا ليس صحيحا تماما - حلقة مع شرط ثابت (مثل while (true) { /* ... */ } يسمح) صراحة المعيار، حتى لو أنه لا يحتوي على أي آثار جانبية. هذا مختلف في C ++. لا تطبق LLVM معيار C بشكل صحيح هنا.

كان لدي انطباع بأنك جادلت أعلاه بأن هذا ليس نفس الخطأ مثل مشكلة الحلقة اللانهائية.

يتم تعريف سلوك برامج Rust غير المنتهية دائمًا ، بينما يتم تحديد سلوك برامج LLVM-IR غير المنتهية فقط في حالة استيفاء شروط معينة.

اعتقدت أن هذه المشكلة تتعلق بإصلاح تطبيق Rust للحلقات اللانهائية بحيث يتم تحديد سلوك LLVM-IR الذي تم إنشاؤه ، ولهذا ، بدا حل جيد جدًا ، @llvm.sideeffect .

ذكر SergioBenitez أنه يمكن للمرء أيضًا إنشاء برامج Rust غير منتهية باستخدام العودية ، وجادل rkruppe بأن العودية اللانهائية والحلقات اللانهائية متكافئة ، بحيث يكون كلاهما نفس الخطأ.

لا أوافق على أن هاتين المسألتين مرتبطتان ، أو حتى أنهما نفس الخطأ ، لكن بالنسبة لي ، تبدو هاتان المسألتان مختلفتين قليلاً:

  • الحل الحكيم ، ننتقل من تطبيق حاجز تحسين ( @llvm.sideeffect ) حصريًا إلى الحلقات غير المنتهية ، لتطبيقه على كل وظيفة Rust واحدة.

  • من حيث القيمة ، اللانهائية loop s مفيدة لأن البرنامج لا ينتهي أبدًا. للتكرار اللانهائي ، يعتمد ما إذا كان البرنامج ينتهي على مستوى التحسين (على سبيل المثال ما إذا كان LLVM يحول التكرار إلى حلقة أم لا) ، ومتى وكيف ينتهي البرنامج يعتمد على النظام الأساسي (حجم المكدس ، صفحة الحماية المحمية ، إلخ). إصلاح كلاهما مطلوب لجعل تطبيق Rust يبدو سليمًا ، ولكن في حالة التكرار اللانهائي ، إذا كان المستخدم ينوي تكرار برنامجه إلى الأبد ، فسيظل التطبيق السليم "خاطئًا" بمعنى أنه لن يتكرر دائمًا إلى الأبد.

الحل الحكيم ، ننتقل من تطبيق حاجز تحسين (@ llvm.sideeffect) حصريًا إلى الحلقات غير المنتهية ، لتطبيقه على كل وظيفة صدأ.

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

إذا فهمت المشكلة بشكل صحيح ، لإصلاح حالة الحلقة اللانهائية ، يجب أن يكفي إدخال llvm.sideeffect في loop { ... } و while <compile-time constant true> { ... } حيث لا يحتوي جسم الحلقة على break تعبيرات. هذا يوضح الفرق بين دلالات C ++ ودلالات الصدأ للحلقات اللانهائية: في Rust ، على عكس C ++ ، لا يُسمح للمترجم بافتراض أن الحلقة تنتهي عندما يكون من الممكن معرفتها في وقت الترجمة أنها لا _ لا _. (لست متأكدًا من مدى الحاجة إلى القلق بشأن التصحيح في مواجهة الحلقات حيث قد يصاب الجسم بالذعر ، ولكن يمكن دائمًا تحسين ذلك لاحقًا.)

لا أعرف ماذا أفعل بشأن العودية اللانهائية ، لكنني أتفق مع RalfJung في أن تحسين التكرار اللانهائي في segfault غير ذي صلة ليس سلوكًا مرغوبًا فيه.

تضمين التغريدة

إذا فهمت المشكلة بشكل صحيح ، لإصلاح حالة الحلقة اللانهائية ، يجب أن يكفي إدراج llvm.sideeffect في الحلقة {...} وأثناء{...} حيث لا يحتوي جسم الحلقة على تعبيرات فاصلة.

لا أعتقد أن الأمر بهذه البساطة ، على سبيل المثال ، loop { if false { break; } } عبارة عن حلقة لا نهائية تحتوي على تعبير break ، ومع ذلك نحتاج إلى إدخال @llvm.sideeffect لمنع llvm من إزالته. AFAICT علينا إدخال @llvm.sideeffect ما لم نثبت أن الحلقة تنتهي دائمًا.

تضمين التغريدة

loop { if false { break; } } حلقة لا نهائية تحتوي على تعبير فاصل ، ومع ذلك نحتاج إلى إدخال @llvm.sideeffect لمنع llvm من إزالته.

حسنًا ، هذا مزعج. لكن ليس علينا أن نكون _perfect_ ، فقط على صواب متحفظ. حلقة مثل

while spinlock.load(Ordering::SeqCst) != 0 {}

(من وثائق std::sync::atomic ) يمكن رؤيتها بسهولة لا تحتاج إلى @llvm.sideeffect ، نظرًا لأن حالة التحكم ليست ثابتة (وعملية الحمل الذري لها حساب أفضل كأثر جانبي لأغراض LLVM ، أو لدينا مشاكل أكبر). نوع الحلقة المحدودة التي قد ينبعثها منشئ البرنامج ،

loop {
    if /* runtime-variable condition */ { break }
    /* more stuff */
}

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

loop {
    if /* provably false at compile time */ { break }
}

؟

اعتقدت أن هذه المشكلة تتعلق بإصلاح تطبيق Rust للحلقات اللانهائية بحيث يتم تحديد سلوك LLVM-IR الذي تم إنشاؤه ، ولهذا ، بدا @ llvm.sideeffect حلاً جيدًا.

عادل بما يكفي. ومع ذلك ، كما قلت ، فإن المشكلة (عدم التطابق بين دلالات Rust و LLVM) تتعلق في الواقع بعدم الإنهاء ، وليس حول الحلقات. لذلك أعتقد أن هذا ما يجب أن نتتبعه هنا.

تضمين التغريدة

إذا فهمت المشكلة بشكل صحيح ، لإصلاح حالة الحلقة اللانهائية ، يجب أن يكفي إدراج llvm.sideeffect في الحلقة {...} وأثناء{...} حيث لا يحتوي جسم الحلقة على تعبيرات فاصلة. هذا يوضح الفرق بين دلالات C ++ ودلالات الصدأ للحلقات اللانهائية: في Rust ، على عكس C ++ ، لا يُسمح للمترجم بافتراض أن الحلقة تنتهي عندما يكون معروفًا في وقت الترجمة أنها لا تنتهي. (لست متأكدًا من مدى الحاجة إلى القلق بشأن التصحيح في مواجهة الحلقات حيث قد يصاب الجسم بالذعر ، ولكن يمكن دائمًا تحسين ذلك لاحقًا.)

ما تصفه يحمل لـ C. في Rust ، يُسمح لأي حلقة أن تتباعد. كل شيء آخر سيكون غير سليم.

لذلك ، على سبيل المثال

while test_fermats_last_theorem_on_some_random_number() { }

هو برنامج جيد في Rust (لكن ليس في C أو C ++) ، وسوف يستمر في التكرار إلى الأبد دون التسبب في آثار جانبية. لذلك ، يجب أن تكون كل الحلقات ، باستثناء تلك التي يمكننا إثبات أنها ستنتهي.

تضمين التغريدة

هل هناك أي حالة أن قاعدة "عدم وجود تعبيرات فاصلة في جسم الحلقة" تصبح خاطئة إلى جانب ذلك

انها ليست فقط if /*compile-time condition */ . يتأثر تدفق التحكم بالكامل ( while ، match ، for ، ...) وتتأثر أيضًا ظروف وقت التشغيل.

لكن لا يتعين علينا أن نكون كاملين ، فقط صحيحين بشكل متحفظ.

يعتبر:

fn foo(x: bool) { loop { if x { break; } } }

حيث x هو شرط وقت التشغيل. إذا لم نصدر @llvm.sideeffect هنا ، ثم إذا كتب المستخدم foo(false) مكان ما ، فيمكن أن يكون foo مضمّنًا ومع الانتشار المستمر وإلغاء الكود الميت ، فإن الحلقة محسّنة إلى حلقة لا نهائية بدون آثار جانبية ، مما يؤدي إلى سوء التحسين.

إذا كان ذلك منطقيًا ، فإن التحويل الذي سيسمح لـ LLVM به هو استبدال foo بـ foo_opt :

fn foo_opt(x: bool) { if x { foo(true) } else { foo(false) } }

حيث يتم تحسين كلا الفرعين بشكل مستقل ، وسيتم إساءة تحسين الفرع الثاني إذا لم نستخدم @llvm.sideeffect .

بمعنى ، لكي نتمكن من حذف @llvm.sideeffect ، سنحتاج إلى إثبات أن LLVM لا يمكنها إساءة تحسين تلك الحلقة تحت أي ظرف من الظروف. الطريقة الوحيدة لإثبات ذلك هي إما إثبات أن الحلقة تنتهي دائمًا ، أو إثبات أنها إذا لم تنتهي ، فإنها تقوم دون قيد أو شرط بأحد الأشياء التي تمنع سوء التحسينات. وحتى مع ذلك ، فإن التحسينات مثل تقسيم / تقشير الحلقة يمكن أن تحول حلقة واحدة إلى سلسلة من الحلقات ، وسيكون كافياً أن لا يكون لدى أحدها @llvm.sideeffect لحدوث سوء تحسين.

يبدو لي كل شيء عن هذا الخطأ أنه سيكون من الأسهل حله من LLVM مقارنةً بـ rustc . (إخلاء المسئولية: لا أعرف حقًا قاعدة التعليمات البرمجية لأي من هذه المشاريع)

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

من ناحية أخرى ، يمكن لـ rustc القيام بذلك فقط بإضافة @llvm.sideeffect ، والذي من المحتمل أن يكون له تأثير أكبر على التحسين من تعطيل التحسينات التي تستخدم عدم الإنهاء "فقط". وسيتعين على rustc تضمين رمز جديد لمحاولة اكتشاف (غير) إنهاء الحلقات.

لذلك أعتقد أن الطريق إلى الأمام سيكون:

  1. أضف @llvm.sideeffect في كل حلقة واستدعاء وظيفة لإصلاح المشكلة
  2. إصلاح LLVM لعدم إجراء تحسينات خاطئة على الحلقات غير المنتهية ، وإزالة @llvm.sideeffects

ما رأيك بهذا؟ آمل ألا يكون تأثير الأداء للخطوة 1 سيئًا للغاية ، على الرغم من ذلك ، حتى لو كان من المفترض أن يتلاشى بمجرد تنفيذ 2 ...

Ekleog هذا ما قد يكون التصحيح الثاني لـ sunfishcode حول: https://lists.llvm.org/pipermail/llvm-dev/2017- أكتوبر/118595.html

جزء من اقتراح سمة الوظيفة هو
قم بتغيير الدلالات الافتراضية لـ LLVM IR لتعريف السلوك على
حلقات لانهائية ، ثم قم بإضافة سمة تختار في UB المحتمل. وبالتالي
إذا فعلنا ذلك ، فإن دور @ llvm.sideeffect يصبح قليلاً
دقيقة - ستكون طريقة للواجهة الأمامية للغة مثل C للاختيار
إلى UB محتمل لوظيفة ما ، ولكن بعد ذلك اختر إلغاء اشتراك فردي
حلقات في هذه الوظيفة.

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

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

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


قد يكون الحل الوسط المحتمل هو إدراج rustc insert @ llvm.sideeffect عندما يكتب المستخدم حرفيًا loop { } فارغًا (أو ما شابه) أو العودية غير المشروطة (التي تحتوي بالفعل على نسالة). سيسمح هذا الحل الوسط للأشخاص الذين يعتزمون فعلاً الحصول على حلقة دوارة لا نهائية وغير فعالة للحصول عليها ، مع تجنب أي حمل إضافي لأي شخص آخر. بالطبع ، لن يجعل هذا الحل الوسط من المستحيل تعطل الكود الآمن ، ولكنه من المحتمل أن يقلل من فرص حدوثه عن طريق الخطأ ، ويبدو أنه من السهل تنفيذه.

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

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

إذا كان لأي تعليمات في الجسم آثار جانبية محتملة ، فقد يكون لجسم الوظيفة آثار جانبية.

هناك سبب وجيه لأن "عدم الإنهاء" يعتبر تأثيرًا عندما تبدأ في النظر إلى الأمور بشكل رسمي. (هاسكل ليست نقية ، لها تأثيران: عدم الإنهاء والاستثناءات.)

قد يكون الحل الوسط المحتمل هو إدراج rustc insert @ llvm.sideeffect عندما يكتب المستخدم حرفيًا حلقة فارغة {} (أو ما شابه ذلك) أو تكرار غير مشروط (يحتوي بالفعل على نسالة). سيسمح هذا الحل الوسط للأشخاص الذين يعتزمون فعلاً الحصول على حلقة دوارة لا نهائية وغير فعالة للحصول عليها ، مع تجنب أي حمل إضافي لأي شخص آخر. بالطبع ، لن يجعل هذا الحل الوسط من المستحيل تعطل الكود الآمن ، ولكنه من المحتمل أن يقلل من فرص حدوثه عن طريق الخطأ ، ويبدو أنه من السهل تنفيذه.

كما لاحظت بنفسك ، هذا لا يزال غير صحيح. لا أعتقد أنه يجب علينا قبول "حل" نعلم أنه غير صحيح. المترجمون هم جزء لا يتجزأ من بنيتنا التحتية ، لا يجب أن نأمل فقط في عدم حدوث أي خطأ. هذه ليست طريقة لبناء أساس متين.


ما حدث هنا هو أن فكرة الصواب بُنيت حول ما فعله المترجمون ، بدلاً من البدء بـ "ماذا نريد من المجمعين لدينا" ثم تحديد مواصفاتهم. لا يحول المترجم الصحيح البرنامج الذي يتباعد دائمًا إلى برنامج ينتهي ، فترة. أجد هذا بديهيًا إلى حد ما ، ولكن مع وجود نظام نوع معقول لـ Rust ، فإن هذا واضح حتى في الأنواع ، وهذا هو سبب ظهور المشكلة بانتظام.

نظرًا للقيود التي نعمل معها (وبالتحديد ، LLVM) ، ما يجب علينا فعله هو البدء بإضافة llvm.sideeffect في أماكن كافية بحيث يضمن كل تنفيذ متباعد "تنفيذ" عدد لا نهائي من هؤلاء. ثم وصلنا إلى خط أساس معقول (كما هو الحال في الصوت والصحيح) ويمكننا التحدث عن التحسينات عن طريق إزالة هذه التعليقات التوضيحية عندما نضمن عدم الحاجة إليها.

لجعل وجهة نظري أكثر دقة ، أعتقد أن ما يلي هو صندوق Rust سليم ، مع pick_a_number_greater_2 عائد (غير حتمي) نوع من العمليات الكبيرة:

fn test_fermats_last_theorem() -> bool {
  let x = pick_a_number_greater_2();
  let y = pick_a_number_greater_2();
  let z = pick_a_number_greater_2();
  let n = pick_a_number_greater_2();
  // x^n + y^n = z^n is impossible for n > 2
  pow(x, n) + pow(y, n) != pow(z, n)
}

pub fn diverge() -> ! {
  while test_fermats_last_theorem() { }
  // This code is unreachable, as proven by Andrew Wiles
  unsafe { mem::transmute(()) }
}

إذا قمنا بتجميع تلك الحلقة المتباينة بعيدًا ، فهذا خطأ ويجب إصلاحه.

ليس لدينا أرقام حتى الآن عن مقدار الأداء الذي سيكلفه إصلاح هذا بسذاجة. حتى نقوم بذلك ، لا أرى أي سبب لكسر برامج مثل المذكورة أعلاه.

من الناحية العملية ، سينتهي fn foo() { foo() } دائمًا بسبب استنفاد الموارد ، ولكن نظرًا لأن آلة Rust abstract لديها إطار مكدس كبير بلا حدود (AFAIK) ، فمن الصحيح تحويل هذا الرمز إلى fn foo() { loop {} } والذي لن يحدث أبدًا تنتهي (أو بعد ذلك بكثير ، عندما يتجمد الكون). هل يجب أن يكون هذا التحول صحيحًا؟ أود أن أقول نعم ، لأنه بخلاف ذلك لا يمكننا إجراء تحسينات استدعاء الذيل ما لم نتمكن من إثبات الإنهاء ، وهذا سيكون أمرًا مؤسفًا.

هل من المنطقي أن يكون لديك unsafe جوهريًا ينص على أن حلقة معينة ، عودية ، ... تنتهي دائمًا؟ يعطي N1528 مثالاً على ذلك ، إذا كان لا يمكن افتراض انتهاء الحلقات ، فلا يمكن تطبيق اندماج الحلقة على رمز المؤشر الذي يجتاز القوائم المرتبطة ، لأن القوائم المرتبطة يمكن أن تكون دائرية ، وإثبات أن القائمة المرتبطة ليست دائرية ليس شيئًا يستطيع المترجمون الحديثون القيام به فعل.

أوافق تمامًا على أننا بحاجة إلى إصلاح مشكلة السلامة هذه إلى الأبد. ومع ذلك ، فإن الطريقة التي نتبعها في ذلك يجب أن تأخذ في الاعتبار إمكانية "إضافة llvm.sideeffect كل مكان لا يمكننا إثبات أنه غير ضروري" قد يؤدي إلى تراجع جودة التعليمات البرمجية للبرامج التي تم تجميعها بشكل صحيح اليوم. في حين أن هذه المخاوف يتم تجاوزها في النهاية بسبب الحاجة إلى وجود مترجم صوت ، فقد يكون من الحكمة المضي قدمًا بطريقة تؤخر الإصلاح المناسب قليلاً في مقابل تجنب تراجع الأداء وتحسين جودة الحياة لمبرمج Rust المتوسط ​​في المتوسط. زمن. انا اقترح:

  • كما هو الحال مع إصلاحات تراجع الأداء المحتملة الأخرى لأخطاء السلامة طويلة الأمد (# 10184) ، يجب علينا تنفيذ الإصلاح خلف علامة Z حتى نتمكن من تقييم تأثير الأداء على قواعد الكود في البرية.
  • إذا تبين أن التأثير غير واضح ، رائع ، فيمكننا تشغيل الإصلاح افتراضيًا.
  • ولكن إذا كانت هناك انحدارات حقيقية منه ، فيمكننا أخذ هذه البيانات إلى الأشخاص LLVM ومحاولة تحسين LLVM أولاً (أو يمكننا اختيار تناول الانحدار وإصلاحه لاحقًا ، ولكن على أي حال ، سنتخذ قرارًا مستنيرًا)
  • إذا قررنا عدم تشغيل الإصلاح افتراضيًا بسبب الانحدار ، فيمكننا على الأقل المضي قدمًا في إضافة llvm.sideeffect إلى الحلقات الفارغة من الناحية التركيبية: إنها شائعة إلى حد ما وقد أدى سوء فهمها إلى إنفاق العديد من الأشخاص بشكل بائس ساعات من تصحيح المشكلات الغريبة (# 38136 ، # 47537 ، # 54214 ، وبالتأكيد هناك المزيد) ، لذلك على الرغم من أن هذا التخفيف ليس له تأثير على خطأ السلامة ، إلا أنه سيكون له فائدة ملموسة للمطورين بينما نعمل على حل مكامن الخلل بالشكل المناسب إصلاح الخلل.

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

في غضون ذلك ، هل ينبغي ذكر ذلك في https://doc.rust-lang.org/beta/reference/behavior-consoded-undefined.html طالما أن هذه المشكلة مفتوحة؟

هل من المنطقي أن يكون لديك unsafe مضمونًا ينص على أن حلقة معينة ، عودية ، ... تنتهي دائمًا؟

std::hint::reachable_unchecked ؟

بالمناسبة واجهت هذا الرمز الحقيقي للكتابة لنظام رسائل TCP. كان لدي حلقة لا نهائية كحلقة مؤقتة حتى أضع آلية حقيقية للتوقف لكن الخيط خرج على الفور.

في حالة رغبة أي شخص في لعب لعبة غولف كود الحالة التجريبية:

fn main() {
    (|| loop {})()
}

""
تشغيل البضائع - الإفراج
تعليمات غير قانونية (الأساسية ملقاة)

في حالة رغبة أي شخص في لعب لعبة غولف كود الحالة التجريبية:

pub fn main() {
   (|| loop {})()
}

مع العلم -Z insert-sideeffect rustc ، الذي تمت إضافته بواسطة sfanxiang في https://github.com/rust-lang/rust/pull/59546 ، يستمر في التكرار :)

قبل:

main:
  ud2

بعد:

main:
.LBB0_1:
  jmp .LBB0_1

بالمناسبة ، تتبع أخطاء LLVM هذا هو https://bugs.llvm.org/show_bug.cgi؟id=965 ، والذي لم أره منشورًا بعد في هذا الموضوع.

التي لم أر نشرها بعد في هذا الموضوع.

https://github.com/rust-lang/rust/issues/28728#issuecomment -331460667 و https://github.com/rust-lang/rust/issues/28728#issuecomment -263956134

RalfJung هل يمكنك تحديث الارتباط التشعبي https://github.com/simnalamburt/snippets/blob/master/rust/src/bin/infinite.rs في وصف المشكلة إلى https://github.com/simnalamburt/snippets/blob /12e73f45f3/rust/infinite.rs هذا؟ تم تعطيل الارتباط التشعبي السابق لفترة طويلة لأنه لم يكن رابطًا ثابتًا. شكر! 😛

simnalamburt القيام به ، شكرا!

يبدو أن زيادة مستوى اختيار MIR لتجنب سوء التحسين في الحالة التالية:

pub fn main() {
   (|| loop {})()
}

--emit=llvm-ir -C opt-level=1

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  unreachable
}

--emit=llvm-ir -C opt-level=1 -Z mir-opt-level=2

define void @_ZN7example4main17hf7943ea78b0ea0b0E() unnamed_addr #0 !dbg !6 {
  br label %bb1, !dbg !10

bb1:                                              ; preds = %bb1, %start
  br label %bb1, !dbg !11
}

https://godbolt.org/z/N7VHnj

rustc 1.45.0-nightly (5fd2f06e9 2020-05-31)

pub fn oops() {
   (|| loop {})() 
}

pub fn main() {
   oops()
}

لقد ساعد في تلك الحالة الخاصة ولكنه لا يحل المشكلة بشكل عام. https://godbolt.org/z/5hv87d

بشكل عام ، لا يمكن حل هذه المشكلة إلا عندما يتمكن كل من rustc أو LLVM من إثبات أن الوظيفة الصرفة كلية قبل استخدام أي تحسينات ذات صلة.

في الواقع ، لم أكن أؤكد أنها حلت المشكلة. كان التأثير الخفي مثيرًا للاهتمام بدرجة كافية للآخرين بحيث بدا أنه يستحق الذكر هنا أيضًا. يستمر -Z insert-sideeffect في تصحيح كلتا الحالتين.

هناك شيء ما يتحرك على جانب LLVM: هناك اقتراح لإضافة سمة على مستوى الوظيفة للتحكم في ضمانات التقدم. https://reviews.llvm.org/D85393

لست متأكدًا من سبب تأكيد الجميع (هنا وفي سلاسل LLVM) على الفقرة المتعلقة بالتقدم إلى الأمام.

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

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

يُسمح بنقل حسابات القيم ، طالما أنها تحدث - قبل استخدام القيمة. الآن ، إذا كان هناك دليل على أنه لا يمكن أن يكون هناك استخدام للقيمة ، فهذا دليل على عدم حدوث ذلك من قبل ، ويمكن نقل الكود بلا حدود بعيدًا في المستقبل ، ولا يزال يلبي نموذج الذاكرة.

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

ليس من المقبول إزالة استدعاء الوظيفة التالي ، حتى إذا كانت النتيجة غير مستخدمة:

fn sideeffect() -> u32 {
  println!("Hello!");
  42
}

fn main() {
  let _ = sideffect(); // May not be removed.
}

هذا صحيح بالنسبة لأي نوع من الآثار الجانبية ، ويظل صحيحًا عند استبدال الطباعة بـ loop {} .

لا تتطلب الادعاء بعدم الإنهاء كأثر جانبي اتفاقًا على أنه (أي غير مثير للجدل) فحسب ، بل يتطلب أيضًا اتفاقًا حول متى يجب ملاحظته.

يتم ملاحظة عدم الإنهاء بالتأكيد ، إذا كانت الحلقة تحسب القيمة. لا يتم ملاحظة عدم الإنهاء ، إذا سمح لك بإعادة ترتيب الحسابات التي لا تعتمد على نتيجة الحلقة.

مثل المثال الموجود على مؤشر ترابط LLVM.

x = y % 42;
if y < 0 return 0;
...

لا علاقة لخصائص الإنهاء للقسمة بإعادة الترتيب. ستحاول وحدات المعالجة المركزية الحديثة تنفيذ التقسيم والمقارنة والتنبؤ بالفرع والجلب المسبق للفرع الناجح بالتوازي. لذلك لا يمكنك ضمان ملاحظة اكتمال التقسيم في الوقت الذي تلاحظ فيه إرجاع 0 ، إذا كان y سلبيًا. (من خلال "مشاهدة" هنا ، أعني القياس حقًا باستخدام مقياس الذبذبات حيث توجد وحدة المعالجة المركزية ، وليس بواسطة البرنامج)

إذا لم تتمكن من ملاحظة أن الانقسام قد اكتمل ، فلا يمكنك ملاحظة بدء الانقسام. لذلك يُسمح عادةً بإعادة ترتيب القسمة في المثال أعلاه ، وهو ما قد يفعله المترجم:

if y < 0 return 0;
x = y % 42;
...

أقول "عادة" ، لأنه ربما توجد لغات لا يُسمح فيها بذلك. لا أعرف ما إذا كانت Rust مثل هذه اللغة.

الحلقات النقية لا تختلف.


انا لا اقول انها ليست مشكلة. أنا أقول فقط إن ضمان التقدم إلى الأمام ليس هو الشيء الذي يسمح بحدوث ذلك.

الادعاء بعدم الإنهاء كأثر جانبي لا يتطلب فقط اتفاقًا (أي غير مثير للجدل) ، بل يتطلب أيضًا اتفاقًا حول متى يجب مراعاته.

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

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

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

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

إذا كانت هذه الأشياء محيرة ، أقترح القيام ببعض القراءة الأساسية. يمكنني أن أوصي بـ "الأنواع ولغات البرمجة" بقلم بنجامين بيرس. من المحتمل أيضًا أن تجد الكثير من منشورات المدونة ، على الرغم من أنه قد يكون من الصعب الحكم على مدى معرفة المؤلف حقًا.

على نحو ملموس ، إذا تم تغيير مثال القسمة الخاص بك إلى

x = 42 % y;
if y <= 0 { return 0; }

ثم آمل أن توافق على أن الشرط _ لا يمكن_ أن يتم رفعه فوق القسمة ، لأن ذلك سيغير السلوك المرئي عندما تكون y صفرًا (من الانهيار إلى إرجاع الصفر).

بنفس الطريقة ، في

x = if y == 0 { loop {} } else { y % 42 };
if y < 0 { return 0; }

تسمح آلة تجريد الصدأ بإعادة كتابة هذا كـ

if y == 0 { loop {} }
else if y < 0 { return 0; }
x = y % 42;

ولكن لا يمكن تجاهل الشرط الأول والحلقة.

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

شكرا لكم لتحمل معي

zackw شكرا لك. هذا رمز مختلف ، والذي سيؤدي بالطبع إلى تحسين مختلف.

فرضيتي حول تحسين الحلقات بنفس الطريقة التي كان بها التقسيم معيبًا (لا يمكنني رؤية نتيجة القسمة == لا أرى الحلقة تنتهي) ، لذا لا يهم الباقي.

olotenko لا أعرف ماذا تقصد ب "مشاهدة نتيجة الحلقة". الحلقة غير النهائية تجعل البرنامج بأكمله يتباعد ، وهو ما يعتبر سلوكًا يمكن ملاحظته - وهذا يعني أنه يمكن ملاحظته خارج البرنامج. كما هو الحال في ، يمكن للمستخدم تشغيل البرنامج ورؤية أنه يستمر إلى الأبد. البرنامج الذي يستمر إلى الأبد قد لا يتم تجميعه في برنامج ينتهي ، لأن ذلك يغير ما يمكن للمستخدم ملاحظته حول البرنامج.

لا يهم ما كانت هذه الحلقة تحسبه أو إذا تم استخدام "القيمة المعادة" للحلقة أم لا. ما يهم هو ما يمكن للمستخدم ملاحظته عند تشغيل البرنامج. يجب أن يتأكد المترجم من بقاء هذا السلوك الذي يمكن ملاحظته على حاله. يعتبر عدم الإنهاء ملحوظًا.

لإعطاء مثال آخر:

fn main() {
  loop {}
  println!("Hello");
}

لن يقوم هذا البرنامج بطباعة أي شيء أبدًا بسبب الحلقة. ولكن إذا قمت بتحسين الحلقة بعيدًا (أو أعدت ترتيب الحلقة بالطباعة) ، فسيقوم البرنامج فجأة بطباعة "Hello". وبالتالي فإن هذه التحسينات تغير السلوك المرئي للبرنامج ، ولا يُسمح بها.

RalfJung لا بأس ، لقد

أحتاج إلى التفكير أكثر في الأمر ، لكنني أعتقد أنني أستطيع أن أرى السبب الذي يجعلنا نستطيع "التظاهر" بحدوث التقسيم في المثال بـ x = y % 42 ، حتى لو لم يتم تنفيذه بالفعل لبعض المدخلات ، ولكن لماذا لا ينطبق الشيء نفسه على الحلقات التعسفية. أعني ، التفاصيل الدقيقة لمراسلات الترتيب الكلي (البرنامج) والأمر الجزئي (التنفيذ).

أعتقد أن "السلوك الذي يمكن ملاحظته" قد يكون أكثر دقة من ذلك بقليل ، حيث سينتهي التكرار اللانهائي بانهيار تجاوز سعة المكدس ("ينتهي" بمعنى "المستخدم الذي يراقب النتيجة") ، ولكن تحسين الاتصال الذيل سوف يحولها إلى حلقة غير منتهية. هذا على الأقل شيء آخر سيفعله Rust / LLVM. لكن لا يتعين علينا مناقشة هذا السؤال لأن هذا ليس في الحقيقة ما كانت مشكلتي تدور حوله (إلا إذا كنت تريد ذلك! أنا متأكد من أنني سعيد لفهم ما إذا كان ذلك متوقعًا).

كومة تجاوز

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

يمكننا إعادة ترتيب (في أمر التنفيذ) فقط التعبيرات التي يمكننا إثبات أنها إنهاء

في الواقع. Morevoer يجب أن تكون "نقية" ، أي خالية من الآثار الجانبية - لا يمكنك إعادة ترتيب اثنين println! . هذا هو السبب في أننا عادة ما نعتبر عدم الإنهاء تأثيرًا أيضًا ، لأنه بعد ذلك يتم تقليل كل هذا إلى "التعبيرات النقية يمكن إعادة ترتيبها" ، و "التعبيرات غير النهائية غير نقية" (غير النقية = لها تأثير جانبي).

من المحتمل أيضًا أن يكون التقسيم غير نقي ، ولكن فقط عند القسمة على 0 - مما يسبب حالة من الذعر ، أي تأثير تحكم. هذا لا يمكن ملاحظته بشكل مباشر ولكن بشكل غير مباشر (على سبيل المثال عن طريق جعل معالج الذعر يطبع شيئًا ما إلى stdout ، والذي يمكن ملاحظته بعد ذلك). وبالتالي لا يمكن إعادة ترتيب القسمة إلا لأننا على يقين من أننا لا نقسم على 0.

لدي بعض الشفرات التجريبية التي أعتقد أنها قد تكون هذه المشكلة ولكني لست متأكدًا تمامًا. إذا لزم الأمر يمكنني وضع هذا في تقرير خطأ جديد.
أضع الكود الخاص به في git repo على https://github.com/uglyoldbob/rust_demo

تم تحسين الحلقة اللانهائية الخاصة بي (مع الآثار الجانبية) ويتم إنشاء تعليمات المصيدة.

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

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

uglyoldbob سيكون ما يحدث بالفعل في llvm-objdump غير مفيد بشكل مذهل (وغير دقيق). يعني هذا bl #4 (وهو في الواقع بناء جملة غير صالح) هنا تفرع إلى 4 بايت بعد نهاية تعليمة bl ، والتي تعرف أيضًا بنهاية الدالة main ، والتي تعرف أيضًا باسم بداية الوظيفة التالية. يتم استدعاء الدالة التالية (عندما نبني عليه) _ZN11broken_loop18__cortex_m_rt_main17hbe300c9f0053d54dE ، وهذا هو الخاص بك الفعلية main وظيفة. الوظيفة التي تحمل الاسم غير المتشابك main ليست وظيفتك ، ولكنها دالة مختلفة تمامًا تم إنشاؤها بواسطة الماكرو #[entry] المقدم بواسطة cortex-m-rt . لم يتم تحسين شفرتك بالفعل. (في الواقع ، لا يعمل المُحسِّن حتى لأنك تقوم بالبناء في وضع التصحيح.)

هل كانت هذه الصفحة مفيدة؟
0 / 5 - 0 التقييمات