สาเหตุของ FatalExecutionEngineError นี้ใน. NET 4.5 เบต้าคืออะไร [ปิด]


150

โค้ดตัวอย่างด้านล่างเกิดขึ้นตามธรรมชาติ ทันใดนั้นโค้ดของฉันก็เป็นFatalExecutionEngineErrorข้อยกเว้นที่น่ารังเกียจ ฉันใช้เวลา 30 นาทีในการพยายามแยกและย่อตัวอย่างผู้ร้ายให้น้อยที่สุด รวบรวมสิ่งนี้โดยใช้ Visual Studio 2012 เป็นแอพคอนโซล:

class A<T>
{
    static A() { }

    public A() { string.Format("{0}", string.Empty); }
}

class B
{
    static void Main() { new A<object>(); }
}

ควรสร้างข้อผิดพลาดนี้ใน. NET Framework 4 และ 4.5:

ภาพหน้าจอ FatalExecutionException

นี่เป็นข้อผิดพลาดที่รู้จักสาเหตุและสิ่งที่ฉันสามารถทำได้เพื่อบรรเทามันได้หรือไม่ งานปัจจุบันของฉันคือไม่ใช้string.Emptyแต่ฉันเห่าต้นไม้ผิด? เปลี่ยนอะไรเกี่ยวกับรหัสที่ทำให้มันทำงานเป็นคุณจะคาดหวัง - ตัวอย่างการถอดคอนสตรัคคงว่างเปล่าAหรือเปลี่ยนประเภทพารามิเตอร์จากไปobjectint

ฉันลองใช้รหัสนี้ในแล็ปท็อปของฉันและมันก็ไม่ได้บ่น อย่างไรก็ตามฉันลองแอพหลักของฉันและมันก็ล้มเหลวบนแล็ปท็อปเช่นกัน ฉันต้องจัดการบางสิ่งบางอย่างเมื่อลดปัญหาฉันจะดูว่าฉันสามารถคิดได้ว่ามันคืออะไร

แล็ปท็อปของฉันทำงานล้มเหลวด้วยรหัสเดียวกันกับข้างบนโดยมีเฟรมเวิร์ก 4.0 แต่เกิดปัญหาหลักแม้กับ 4.5 ทั้งสองระบบใช้ VS'12 พร้อมอัปเดตล่าสุด (กรกฎาคม?)

ข้อมูลเพิ่มเติม :

  • รหัส IL (Debug ที่คอมไพล์แล้ว / CPU ใด ๆ / 4.0 / VS2010 (ไม่ใช่ IDE ที่ควรสำคัญ?)): http://codepad.org/boZDd98E
  • ไม่เห็น VS 2010 พร้อม 4.0 ไม่ขัดข้องด้วย / ไม่มีการเพิ่มประสิทธิภาพ CPU เป้าหมายที่แตกต่างกันดีบักเกอร์ที่แนบมา / ไม่ได้เชื่อมต่อ ฯลฯ - Tim Medora
  • เกิดปัญหาในปี 2010 ถ้าฉันใช้ AnyCPU ก็ใช้ได้ใน x86 เกิดปัญหาใน Visual Studio 2010 SP1 โดยใช้แพลตฟอร์มเป้าหมาย = AnyCPU แต่ใช้ได้กับแพลตฟอร์มเป้าหมาย = x86 เครื่องนี้มีการติดตั้ง VS2012RC เช่นกันดังนั้น 4.5 อาจทำการแทนที่แบบแทนที่ ใช้ AnyCPU และ TargetPlatform = 3.5 ดังนั้นจึงไม่ผิดพลาดดังนั้นดูเหมือนว่าการถดถอยใน Framework.- colinsmith
  • ไม่สามารถทำซ้ำใน x86, x64 หรือ AnyCPU ใน VS2010 ที่มี 4.0 - ฟูจิ
  • เกิดขึ้นเฉพาะกับ x64, (2012rc, Fx4.5) - Henk Holterman
  • VS2012 RC บน Win8 RP เริ่มแรกไม่เห็น MDA นี้เมื่อกำหนดเป้าหมาย. NET 4.5 เมื่อเปลี่ยนเป็นการกำหนดเป้าหมาย. NET 4.0 MDA จะปรากฏขึ้น จากนั้นหลังจากเปลี่ยนกลับเป็น. NET 4.5 MDA ยังคงอยู่ - เวย์น

ฉันไม่เคยรู้มาก่อนว่าคุณสามารถสร้างคอนสตรัคชันคงที่พร้อมกับสาธารณะ Heck ฉันไม่เคยรู้ว่ามีตัวสร้างแบบคงที่อยู่
โคลจอห์นสัน

ฉันมีความคิด: เพราะคุณกำลังเปลี่ยน B จากการเป็นคลาสแบบสแตติกเป็นคลาสที่มีสแตติกหลักใช่หรือไม่
โคลจอห์นสัน

@ChrisSinclair ฉันไม่คิดอย่างนั้น ฉันหมายความว่าฉันทดสอบรหัสนี้ในแล็ปท็อปของฉันและได้ผลลัพธ์เดียวกัน
Gleno

@ColeJohnson ใช่ IL ตรงกันทั้งหมด แต่ที่เดียวที่เห็นได้ชัด ดูเหมือนจะไม่มีข้อผิดพลาดใด ๆ ที่นี่ในคอมไพเลอร์ c #
Michael Graczyk

14
ขอบคุณทั้งโปสเตอร์ต้นฉบับที่รายงานที่นี่และ Michael สำหรับการวิเคราะห์ที่ยอดเยี่ยมของเขา คู่ของฉันใน CLR พยายามที่จะทำซ้ำข้อผิดพลาดที่นี่และค้นพบว่ามันทำซ้ำในรุ่น "Release Candidate" ของ 64 บิต CLR แต่ไม่ได้อยู่ในรุ่น "ปล่อยตัวสู่การผลิต" ขั้นสุดท้ายซึ่งมีจำนวนของการแก้ไขข้อผิดพลาด RC (รุ่น RTM จะเผยแพร่สู่สาธารณะในวันที่ 15 สิงหาคม 2012) พวกเขาเชื่อว่านี่เป็นปัญหาเดียวกับที่รายงานที่นี่: connect.microsoft.com/VisualStudio/feedback/details/737108/…
Eric Lippert

คำตอบ:


114

นี่ไม่ใช่คำตอบที่สมบูรณ์ แต่ฉันมีความคิดเล็กน้อย

ฉันเชื่อว่าฉันพบคำอธิบายที่ดีเท่าที่เราจะหาได้โดยไม่มีใครสักคนจากทีมงาน.

UPDATE

ฉันมองลึกลงไปอีกเล็กน้อยและฉันเชื่อว่าฉันได้พบแหล่งที่มาของปัญหา มันดูเหมือนว่าจะเกิดจากการรวมกันของข้อผิดพลาดในตรรกะเริ่มต้นประเภท JIT และการเปลี่ยนแปลงในคอมไพเลอร์ C # ที่ขึ้นอยู่กับสมมติฐานที่ว่า JIT ทำงานตามที่ตั้งใจไว้ ฉันคิดว่าข้อผิดพลาด JIT มีอยู่ใน. NET 4.0 แต่ถูกเปิดเผยโดยการเปลี่ยนแปลงในคอมไพเลอร์สำหรับ. NET 4.5

ฉันไม่คิดว่าbeforefieldinitเป็นปัญหาเดียวที่นี่ ฉันคิดว่ามันง่ายกว่านั้น

ประเภท System.Stringใน mscorlib.dll จาก. NET 4.0 ประกอบด้วยตัวสร้างแบบคงที่:

.method private hidebysig specialname rtspecialname static 
    void  .cctor() cil managed
{
  // Code size       11 (0xb)
  .maxstack  8
  IL_0000:  ldstr      ""
  IL_0005:  stsfld     string System.String::Empty
  IL_000a:  ret
} // end of method String::.cctor

ใน mscorlib.dll รุ่น. NET 4.5 String.cctor (ตัวสร้างแบบคงที่) ขาดหายไปอย่างชัดเจน:

..... ไม่มีตัวสร้างแบบคงที่ :( .....

ในทั้งสองเวอร์ชันStringประเภทจะประดับด้วยbeforefieldinit:

.class public auto ansi serializable sealed beforefieldinit System.String

ฉันพยายามสร้างประเภทที่จะคอมไพล์กับ IL ในทำนองเดียวกัน (เพื่อให้มันมีเขตข้อมูลแบบคงที่ แต่ไม่มีตัวสร้างแบบคงที่.cctor) แต่ฉันไม่สามารถทำได้ ประเภททั้งหมดเหล่านี้มี.cctorวิธีการใน IL:

public class MyString1 {
    public static MyString1 Empty = new MyString1();        
}

public class MyString2 {
    public static MyString2 Empty = new MyString2();

    static MyString2() {}   
}

public class MyString3 {
    public static MyString3 Empty;

    static MyString3() { Empty = new MyString3(); } 
}

ฉันเดาว่าสองสิ่งเปลี่ยนไประหว่าง. NET 4.0 และ 4.5:

ครั้งแรก: EE มีการเปลี่ยนแปลงเพื่อที่จะเริ่มต้นโดยอัตโนมัติString.Emptyจากรหัสที่ไม่มีการจัดการ อาจมีการเปลี่ยนแปลงนี้สำหรับ. NET 4.0

ข้อที่สอง: คอมไพเลอร์เปลี่ยนไปเพื่อที่มันจะไม่ปล่อยตัวสร้างสแตติกสำหรับสตริงโดยรู้ว่า String.Emptyจะถูกกำหนดจากด้านที่ไม่มีการจัดการ การเปลี่ยนแปลงนี้ดูเหมือนจะเกิดขึ้นกับ. NET 4.5

ปรากฏว่า EE ไม่ได้กำหนดString.Emptyเร็วพอตามเส้นทางการปรับให้เหมาะสมสูงสุด การเปลี่ยนแปลงที่เกิดขึ้นกับคอมไพเลอร์ (หรือสิ่งที่เปลี่ยนแปลงเพื่อทำให้String.cctorหายไป) คาดว่า EE จะทำการมอบหมายนี้ก่อนที่รหัสผู้ใช้จะดำเนินการ แต่ปรากฏว่า EE ไม่ได้ทำการมอบหมายนี้มาก่อนString.Emptyจะใช้ในวิธีการ

สุดท้ายฉันเชื่อว่าข้อผิดพลาดนั้นบ่งบอกถึงปัญหาที่ลึกกว่าในตรรกะการเริ่มต้นประเภท JIT ดูเหมือนว่าการเปลี่ยนแปลงในคอมไพเลอร์เป็นกรณีพิเศษสำหรับSystem.Stringแต่ฉันสงสัยว่า JIT ได้ทำกรณีพิเศษที่นี่เพื่อSystem.Stringได้ทำให้เป็นกรณีพิเศษที่นี่สำหรับ

เป็นต้นฉบับ

ก่อนอื่นเลยว้าวคน BCL มีความคิดสร้างสรรค์อย่างมากพร้อมการเพิ่มประสิทธิภาพบางอย่าง หลายของStringวิธีการที่จะดำเนินการในขณะนี้ใช้แคชคงกระทู้StringBuilderวัตถุ

ฉันติดตามลูกค้าเป้าหมายนั้นมาระยะหนึ่งแล้ว แต่StringBuilderไม่ได้ใช้กับTrimพา ธ โค้ดดังนั้นฉันจึงตัดสินใจว่ามันจะไม่ใช่ปัญหาคงที่ของเธรด

ฉันคิดว่าฉันพบอาการแปลก ๆ ของข้อผิดพลาดเดียวกัน

รหัสนี้ล้มเหลวด้วยการละเมิดการเข้าถึง:

class A<T>
{
    static A() { }

    public A(out string s) {
        s = string.Empty;
    }
}

class B
{
    static void Main() { 
        string s;
        new A<object>(out s);
        //new A<int>(out s);
        System.Console.WriteLine(s.Length);
    }
}

อย่างไรก็ตามหากคุณไม่ใส่เครื่องหมายข้อคิดเห็น//new A<int>(out s);ไว้Mainรหัสก็ใช้งานได้ดี ในความเป็นจริงหากAมีการ reified กับชนิดอ้างอิงใด ๆ โปรแกรมล้มเหลว แต่ถ้าAreified ด้วยค่าชนิดใด ๆ แล้วรหัสไม่ล้มเหลว นอกจากนี้หากคุณแสดงความคิดเห็นคอนAสตรัคเตอร์คงที่ของรหัสจะไม่ล้มเหลว หลังจากขุดลงไปTrimและFormatก็เป็นที่ชัดเจนว่าปัญหาที่ว่าLengthจะถูก inlined และในตัวอย่างเหล่านี้ข้างต้นStringประเภทยังไม่ได้รับการเริ่มต้น โดยเฉพาะอย่างยิ่งภายในร่างกายของAคอนสตรัค 's, string.Emptyไม่ได้ถูกกำหนดอย่างถูกต้องถึงแม้ว่าภายในร่างกายของMain, string.Emptyที่ได้รับมอบหมายได้อย่างถูกต้อง

มันน่าอัศจรรย์สำหรับฉันที่การกำหนดค่าเริ่มต้นของชนิดStringขึ้นอยู่กับว่าAจะมีการปรับเปลี่ยนประเภทค่าหรือไม่ ทฤษฎีเดียวของฉันคือมีบางเส้นทางรหัส JIT เพิ่มประสิทธิภาพสำหรับการเริ่มต้นประเภททั่วไปที่ใช้ร่วมกันในทุกประเภทและเส้นทางที่ทำให้สมมติฐานเกี่ยวกับประเภทอ้างอิง BCL ("ประเภทพิเศษ?") และรัฐของพวกเขา ดูอย่างรวดเร็วแม้ว่าคลาส BCL อื่น ๆ ที่มีpublic staticฟิลด์แสดงให้เห็นว่าโดยทั่วไปแล้วพวกเขาทั้งหมดใช้ตัวสร้างแบบคงที่ (แม้จะมีตัวสร้างที่ว่างเปล่าและไม่มีข้อมูลเช่นSystem.DBNullและSystem.Empty. ประเภทค่า BCL กับpublic staticเขตข้อมูลดูเหมือนจะไม่ใช้ตัวสร้างแบบคงที่System.IntPtrเป็นต้น) ดูเหมือนว่าสิ่งนี้จะบ่งชี้ว่า JIT สร้างสมมติฐานบางอย่างเกี่ยวกับการเริ่มต้นประเภทการอ้างอิง BCL

FYI นี่คือรหัส JITed สำหรับทั้งสองเวอร์ชัน:

A<object>.ctor(out string):

    public A(out string s) {
00000000  push        rbx 
00000001  sub         rsp,20h 
00000005  mov         rbx,rdx 
00000008  lea         rdx,[FFEE38D0h] 
0000000f  mov         rcx,qword ptr [rcx] 
00000012  call        000000005F7AB4A0 
            s = string.Empty;
00000017  mov         rdx,qword ptr [FFEE38D0h] 
0000001e  mov         rcx,rbx 
00000021  call        000000005F661180 
00000026  nop 
00000027  add         rsp,20h 
0000002b  pop         rbx 
0000002c  ret 
    }

A<int32>.ctor(out string):

    public A(out string s) {
00000000  sub         rsp,28h 
00000004  mov         rax,rdx 
            s = string.Empty;
00000007  mov         rdx,12353250h 
00000011  mov         rdx,qword ptr [rdx] 
00000014  mov         rcx,rax 
00000017  call        000000005F691160 
0000001c  nop 
0000001d  add         rsp,28h 
00000021  ret 
    }

ส่วนที่เหลือของรหัส ( Main) เหมือนกันระหว่างสองรุ่น

แก้ไข

นอกจากนี้ IL จากทั้งสองรุ่นเป็นเหมือนกันยกเว้นสำหรับการเรียกร้องให้A.ctorในB.Main()ที่ IL สำหรับรุ่นแรกประกอบด้วย:

newobj     instance void class A`1<object>::.ctor(string&)

กับ

... A`1<int32>...

ในวินาที

สิ่งที่ควรทราบอีกประการหนึ่งคือรหัส JITed สำหรับA<int>.ctor(out string): เหมือนกันกับในรุ่นที่ไม่ใช่ทั่วไป


3
ฉันค้นหาคำตอบตามเส้นทางที่คล้ายกันมาก แต่ดูเหมือนจะไม่นำไปสู่ทุกที่ ดูเหมือนว่าจะเป็นปัญหาคลาสสตริงและหวังว่าจะไม่เป็นปัญหาทั่วไปอีกต่อไป ตอนนี้ฉันกำลังรอใครบางคน (Eric) ที่มีซอร์สโค้ดมาพร้อมและอธิบายสิ่งที่ผิดพลาดและหากมีสิ่งอื่นใดเกิดขึ้นอีก ในฐานะที่เป็นผลประโยชน์เล็ก ๆ การสนทนานี้ตัดสินแล้วการอภิปรายว่าควรใช้string.Emptyหรือ""... :)
Gleno

IL ระหว่างพวกเขาเหมือนกันหรือไม่
โคลจอห์นสัน

49
การวิเคราะห์ที่ดี! ฉันจะส่งให้ทีม BCL ขอบคุณ!
Eric Lippert

2
@EricLippert และคนอื่น ๆ : ฉันค้นพบโค้ดเช่นนั้นtypeof(string).GetField("Empty").SetValue(null, "Hello world!"); Console.WriteLine(string.Empty);จะให้ผลลัพธ์ที่แตกต่างกันใน. NET 4.0 กับ. NET 4.5 การเปลี่ยนแปลงนี้เกี่ยวข้องกับการเปลี่ยนแปลงที่อธิบายไว้ข้างต้นหรือไม่ . NET 4.5 จะเพิกเฉยต่อการเปลี่ยนแปลงค่าฟิลด์ทางเทคนิคได้อย่างไร บางทีฉันควรถามคำถามใหม่เกี่ยวกับเรื่องนี้?
Jeppe Stig Nielsen

4
@JeppeStigNielsen: คำตอบสำหรับคำถามของคุณคือ: "อาจจะ", "ค่อนข้างง่าย, ชัดเจน" และ "นี่เป็นเว็บไซต์คำถามและคำตอบดังนั้นใช่นั่นเป็นความคิดที่ดีถ้าคุณต้องการคำตอบสำหรับคำถามของคุณดีกว่า กว่า 'อาจ' "
Eric Lippert

3

ฉันสงสัยอย่างยิ่งว่าเกิดจากการเพิ่มประสิทธิภาพนี้ (เกี่ยวข้องกับBeforeFieldInit)ใน. NET 4.0

ถ้าฉันจำได้ถูกต้อง:

เมื่อคุณประกาศตัวสร้างแบบคงที่อย่างชัดเจนbeforefieldinitถูกปล่อยออกมาบอกรันไทม์ที่คอนสตรัคคงต้องทำงานก่อนที่จะเข้าถึงสมาชิกคงใด

ฉันเดา:

ฉันเดาว่าพวกเขาอย่างใดเมาขึ้นจริงนี้บน x64 JITer เพื่อที่ว่าเมื่อที่แตกต่างกันประเภทของสมาชิกคงมีการเข้าถึงจากชั้นเรียนที่มีตัวเองคงคอนสตรัคได้ทำงานแล้วมันอย่างใดข้ามทำงาน (หรือรันในลำดับที่ผิด) ตัวสร้างแบบคงที่ - และทำให้เกิดความผิดพลาด (คุณไม่ได้รับข้อยกเว้นตัวชี้โมฆะอาจเป็นไปได้เพราะมันไม่ได้เริ่มต้นด้วย null)

ฉันไม่ได้เรียกใช้รหัสของคุณดังนั้นส่วนนี้อาจผิด - แต่ถ้าฉันต้องคาดเดาอีกครั้งฉันจะบอกว่ามันอาจเป็นสิ่งที่string.Format(หรือConsole.WriteLineที่คล้ายกัน) ต้องเข้าถึงภายในซึ่งเป็นสาเหตุของความผิดพลาดเช่น บางทีคลาสที่เกี่ยวข้องกับโลแคลซึ่งต้องการการสร้างแบบสแตติกที่ชัดเจน

อีกครั้งฉันยังไม่ได้ทดสอบ แต่เป็นการคาดเดาที่ดีที่สุดสำหรับข้อมูล

อย่าลังเลที่จะทดสอบสมมติฐานของฉันและแจ้งให้ฉันทราบว่ามันเป็นอย่างไร


ข้อผิดพลาดยังคงเกิดขึ้นเมื่อBไม่มีตัวสร้างแบบคงที่และจะไม่เกิดขึ้นเมื่อAมีการแก้ไขด้วยประเภทค่า ฉันคิดว่ามันซับซ้อนกว่านี้เล็กน้อย
Michael Graczyk

@MichaelGraczyk: ฉันคิดว่าฉันสามารถอธิบายได้ (อีกครั้งด้วยการเดา) Bมีตัวสร้างแบบคงที่ไม่สำคัญมาก เนื่องจากAมี ctor แบบสแตติกรันไทม์จะยุ่งกับลำดับที่มันถูกรันเมื่อเปรียบเทียบกับคลาสที่เกี่ยวข้องกับโลแคลในเนมสเปซอื่น ดังนั้นเขตข้อมูลนั้นยังไม่ได้เริ่มต้น อย่างไรก็ตามถ้าคุณสร้างอินสแตนซ์Aกับประเภทของค่าอาจเป็นครั้งที่สองของการส่งผ่านรันไทม์A(CLR มีแนวโน้มที่จะถูกทำให้เป็นอินสแตนซ์ล่วงหน้าพร้อมกับประเภทการอ้างอิงเป็นการเพิ่มประสิทธิภาพ) ดังนั้นลำดับจะทำงานเมื่อมันเป็นครั้งที่สอง .
user541686

@MichaelGraczyk: แม้ว่านี่จะไม่ใช่คำอธิบาย แต่ฉันคิดว่าฉันค่อนข้างมั่นใจว่าการbeforefieldinitเพิ่มประสิทธิภาพที่กำหนดเป็นสาเหตุหลัก อาจเป็นได้ว่าคำอธิบายที่แท้จริงบางอย่างนั้นแตกต่างจากที่ฉันพูดถึง แต่สาเหตุที่เป็นไปได้ก็เหมือนกัน
user541686

ฉันดู IL มากขึ้นและฉันคิดว่าคุณกำลังทำบางสิ่ง A<object>.ctor()ฉันไม่คิดว่าความคิดผ่านที่สองเป็นไปได้ที่เกี่ยวข้องที่นี่เพราะรหัสยังคงล้มเหลวถ้าผมทำสายจำนวนมากโดยพลการ
Michael Graczyk

@MichaelGraczyk: ดีใจที่ได้ยินและขอบคุณสำหรับการทดสอบ ฉันไม่สามารถทำซ้ำบนแล็ปท็อปของฉันได้ (2010 4.0 x64) คุณสามารถตรวจสอบเพื่อดูว่ามันเกี่ยวข้องกับการจัดรูปแบบสตริง (เช่นที่เกี่ยวข้องกับโลแคล) หรือไม่? จะเกิดอะไรขึ้นถ้าคุณลบส่วนนั้นออก
user541686

1

ข้อสังเกต แต่ DotPeek แสดงให้เห็นถึงสตริงที่แปลแล้วดังนั้น:

/// <summary>
/// Represents the empty string. This field is read-only.
/// </summary>
/// <filterpriority>1</filterpriority>
[__DynamicallyInvokable]
public static readonly string Empty;

internal sealed class __DynamicallyInvokableAttribute : Attribute
{
  [TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
  public __DynamicallyInvokableAttribute()
  {
  }
}

ถ้าฉันประกาศตัวเองEmptyในแบบเดียวกันยกเว้นไม่มีแอตทริบิวต์ฉันจะไม่ได้รับ MDA อีกต่อไป:

class A<T>
{
    static readonly string Empty;

    static A() { }

    public A()
    {
        string.Format("{0}", Empty);
    }
}

และด้วยคุณสมบัตินั้น เราได้สร้าง""มันขึ้นมาแล้ว
Henk Holterman

แอตทริบิวต์ "Performance critical ... " นั้นมีผลต่อ Constructor ของตัวมันเองไม่ใช่วิธีที่แอตทริบิวต์นั้นประดับ
Michael Graczyk

มันอยู่ภายใน เมื่อฉันกำหนดแอตทริบิวต์ที่เหมือนกันของตัวเองมันก็ยังไม่ทำให้เกิด MDA ไม่ว่าฉันจะคาดหวังว่า - หาก JITter กำลังมองหาคุณลักษณะเฉพาะนั้นจะไม่พบของฉัน
lesscode
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.