في الأجزاء الأربعة السابقة ، تم إجراء الاستعدادات للتجارب مع مركز RISC-V RocketChip ، أي نقل هذا اللب إلى لوحة "غير قياسية" له مع Altera FPGAs (الآن Intel). أخيرًا ، في الجزء الأخير ، اتضح تشغيل Linux على هذا المنتدى. هل تعرف ما يضحكني عن كل هذا؟ حقيقة أنه في نفس الوقت اضطررت إلى العمل مع المجمّع RISC-V و C و Scala ، وكلهم ، كانت Scala هي أدنى مستوى لغة (لأن المعالج مكتوب عليها).
دعونا نجعل C لا تكون مسيئة في هذه المقالة أيضًا. علاوة على ذلك ، إذا تم استخدام حزمة Scala + Chisel كلغة خاصة بمجال معين لوصف صريح للأجهزة ، فسنتعلم اليوم كيفية "سحب" وظائف C البسيطة في المعالج في شكل تعليمات.
الهدف النهائي هو التطبيق التافه للأجهزة التافهة مثل AFL عن طريق القياس مع QInst ، وتنفيذ تعليمات منفصلة ليست سوى منتج ثانوي.
فمن الواضح أن هناك (وليس واحد) التجاري OpenCL لتحويل RTL. لقد صادفت أيضًا معلومات حول مشروع معين لـ COPILOT لـ RISC-V له أهداف مماثلة (أكثر تقدماً) ، ولكن هناك شيئًا ما يصيب غوغلينغ بشكل سيء ، علاوة على ذلك ، فهو أيضًا منتج تجاري على الأرجح. أنا مهتم في المقام الأول بحلول OpenSource ، لكن حتى إذا كان الأمر كذلك ، فلا يزال من الممتع محاولة تنفيذ ذلك بنفسك - على الأقل كدراسة حالة مبسطة ، ثم كيف تسير الأمور ...
إخلاء المسئولية (بالإضافة إلى التحذير المعتاد حول "الرقص باستخدام مطفأة حريق"): أنا لا أوصي بشدة بتطبيق النواة الأساسية للبرامج بشكل متهور ، لا سيما مع البيانات غير الموثوق بها - حتى الآن لم أثق كثيرًا في الثقة حتى أنني لا أفهم لماذا لا يمكن حتى معالجة البيانات قيد المعالجة "التدفق" في بعض الحالات الحدودية بين العمليات و / أو الأساسية. حسنًا ، حول حقيقة أن البيانات يمكن أن "تغلب" ، على ما أعتقد ، وهذا واضح. بشكل عام ، لا يزال هناك التحقق من الصحة والتحقق ...
بالنسبة للمبتدئين ، ماذا أسمي "وظيفة بسيطة"؟ لأغراض هذه المقالة ، يعني هذا وظيفة لا تؤدي فيها جميع التحولات (الشرطية وغير المشروطة) إلا إلى زيادة عداد التعليمات بقيمة ثابتة. بمعنى أن الرسم البياني لجميع التحولات المحتملة يكون (موجهًا) شاذًا ، بدون حواف "ديناميكية". الهدف النهائي في إطار هذه المقالة هو أن تكون قادرًا على القيام بوظيفة بسيطة من البرنامج ، واستبدالها بمكون إضافي للمجمع ، "خياطتها" إلى المعالج في مرحلة التوليف ، مما يجعلها اختيارًا تأثيرًا جانبيًا لتعليمات أخرى. على وجه التحديد ، لن يتم عرض المتفرعة في هذه المقالة ، ولكن في أبسط الحالات لن يكون من الصعب إجراؤها.
تعلم فهم C (في الواقع ، لا)
أولا تحتاج إلى فهم كيف سنقوم بتحليل C؟ هذا صحيح ، بأي حال من الأحوال - لم يكن عبثًا أن تعلمت تحليل ملفات ELF : كل ما تحتاجه هو تجميع التعليمات البرمجية الخاصة بنا في C / Rust / شيء آخر إلى رمز eBPF ثانوي ، وتحليله بالفعل. بعض الصعوبات ناتجة عن حقيقة أنه في Scala لا يمكنك فقط الاتصال بـ elf.h
وقراءة حقول البنية. يمكنك بالطبع استخدام JNAerator - إذا لزم الأمر ، فيمكنهم عمل المجلدات إلى المكتبة - ليس فقط الهياكل ، ولكن أيضًا إنشاء رمز للعمل من خلال JNA (يجب عدم الخلط بينه وبين JNI). كمبرمج حقيقي ، سأكتب دراجتي وأكتب بعناية ثوابت التعداد والإزاحة من ملف الرأس. يتم وصف النتائج والبنى الوسيطة بالهيكل التالي لفئات الحالات:
sealed trait SectionKind case object RegularSection extends SectionKind case object SymtabSection extends SectionKind case object StrtabSection extends SectionKind case object RelSection extends SectionKind final case class Elf64Header( sectionHeaders: Seq[ByteBuffer], sectionStringTableIndex: Int ) final case class Elf64Section( data: ByteBuffer, linkIndex: Int, infoIndex: Int, kind: SectionKind ) final case class Symbol( name: String, value: Int, size: Int, shndx: Int, isInstrumenter: Boolean ) final case class Relocation( relocatedSection: Int, offset: Int, symbol: Symbol ) final case class BpfInsn( opcode: Int, dst: Int, src: Int, offset: Int, imm: Either[Long, Symbol] ) final case class BpfProg( name: String, insns: Seq[BpfInsn] )
لن أصف بشكل خاص عملية التحليل - هذه مجرد عملية نقل باهتة مملة من java.nio.ByteBuffer
- تم بالفعل java.nio.ByteBuffer
جميع الأشياء المثيرة للاهتمام في المقالة حول تحليل ملفات ELF . سأقول فقط أنك بحاجة إلى التعامل مع opcode == 0x18
بعناية opcode == 0x18
(تحميل قيم فورية 64 بت في السجل) ، لأنه يستغرق كلمتين من 8 بايت في آن واحد (ربما هناك رموز تشغيل أخرى من هذا القبيل ، لكنني لم أتطرق إليها بعد) ، وهذا لا يتم دائمًا تحميل عنوان الذاكرة المرتبط بالنقل ، كما اعتقدت في البداية. على سبيل المثال ، يستخدم __builtin_popcountl
بصدق ثابت 64 بت 0x0101010101010101
. لماذا لا أقوم بعملية نقل "صادقة" مع تصحيح الملف الذي تم تنزيله - لأنني أريد أن أرى الأحرف في شكل رمزي (آسف للتورية) ، بحيث يمكن استبدال الأحرف من قسم COMMON
في وقت لاحق COMMON
دون استخدام عكازات مع معالجة خاصة لعناوين من نوع خاص (و وهذا يعني ، حتى مع الرقصات مع UInt
ثابت / غير ثابت).
نحن نبني الأجهزة وفقا لمجموعة من التعليمات
لذلك ، بافتراض أن جميع مسارات التنفيذ الممكنة تنحصر حصريًا في قائمة الإرشادات ، مما يعني أن البيانات تتدفق عبر رسم بياني موجه موجه ، ويتم تحديد جميع حوافها بشكل ثابت. في الوقت نفسه ، لدينا منطق دمجي بحت (أي بدون سجلات في الطريق) ، تم الحصول عليه من العمليات على السجلات ، وكذلك التأخيرات أثناء عمليات التحميل / التخزين مع الذاكرة. وبالتالي ، في الحالة العامة ، قد لا تكون العملية ممكنة في دورة ساعة واحدة. سنفعل شيئًا بسيطًا: UInt
القيمة إلى شكل UInt
، ولكن مثل (UInt, Bool)
: العنصر الأول من الزوج هو القيمة ، والثاني هو علامة على صحتها. بمعنى أنه لا معنى للقراءة من الذاكرة ، طالما أن العنوان غير صحيح ، والكتابة بشكل عام أمر مستحيل.
يفترض طراز تنفيذ eBPF bytecode نوعًا من ذاكرة الوصول العشوائي مع عنونة 64 بت ، بالإضافة إلى مجموعة من 16 (أو حتى 10) سجلات 64 بت. تُقترح خوارزمية متكررة بدائية:
- نبدأ مع السياق الذي تكمن فيه معاملات التعليمة في
r1
و r2
، في الباقي - الأصفار ، كلها صالحة (بتعبير أدق ، الصلاحية تساوي "جاهزية" تعليمة coprocessor) - إذا رأينا إرشادات حسابية منطقية ، فإننا نستخلص سجلات معاملاتها من السياق ، ندعو أنفسنا إلى ذيل القائمة والسياق الذي يتم فيه استبدال معامل التعامل مع زوج
(data1 op data2, valid1 && valid2)
- إذا واجهنا فرعا ، فنحن ببساطة نبني كلا الفرعين بشكل متكرر: إذا حدث الفرع ، وإذا لم يحدث ذلك
- إذا واجهنا التحميل أو الحفظ في الذاكرة ، فسنخرج بطريقة أو بأخرى: ننفذ رد الاتصال المنقول ، مع افتراض أنه بمجرد عدم إمكانية استرجاع البيان
valid
أثناء تنفيذ هذا الإرشادات. صلاحية عملية الحفظ هي AND globalValid
علامة globalValid
، والتي يجب ضبطها قبل إرجاع السيطرة. في الوقت نفسه ، يجب علينا القيام بالكتابة والقراءة على طول المقدمة من أجل معالجة الزيادات والتعديلات الأخرى بشكل صحيح.
وبالتالي ، سيتم تنفيذ العمليات بالتوازي قدر الإمكان ، وليس في خطوات. في الوقت نفسه ، أطلب منك الانتباه إلى أن جميع العمليات على بايت معين من الذاكرة يجب أن يتم ترتيبها بالكامل بشكل طبيعي ، وإلا فإن النتيجة غير متوقعة ، UB. أي *addr += 1
- هذا طبيعي ، لن تبدأ الكتابة تمامًا حتى تكتمل القراءة (مبتذل لأننا ما زلنا لا نعرف ماذا نكتب) ، لكن *addr += 1; return *addr;
*addr += 1; return *addr;
أنا عموما أعطيت الصفر أو شيء من هذا القبيل بأمان. ربما يكون الأمر يستحق التصحيح (ربما يخفي بعض المشاكل الأكثر صعوبة) ، لكن النداء نفسه في أي حال فكرة على هذا النحو ، لأنه يجب عليك تتبع عناوين الذاكرة التي تم إنجاز العمل بها بالفعل ، لكن لدي رغبة التحقق من صحة القيم valid
ممكنا بشكل ثابت. هذا هو بالضبط ما سيتم عمله للمتغيرات العالمية ذات الحجم الثابت.
والنتيجة هي فئة تجريدية BpfCircuitConstructor
، والتي لا يوجد بها طرق doMemLoad
، doMemStore
، و doMemStore
، و resolveSymbol
:
trait BpfCircuitConstructor {
التكامل وحدة المعالجة المركزية الأساسية
بالنسبة للمبتدئين ، قررت اتباع الطريقة البسيطة: الاتصال بنواة المعالج باستخدام بروتوكول RoCC (Rocket Custom Coprocessor) القياسي. بقدر ما أفهم ، هذا امتداد منتظم ليس لجميع حبيبات RISC-V المتوافقة ، ولكن فقط لـ Rocket و BOOM (Berkeley Out-of-Order Machine) ، لذلك ، عند سحب العمل في المراحل الأولى على المجمعين ، تم custom0
التجميع custom0
خارجها. custom3
مسؤولة عن أوامر مسرع.
بشكل عام ، يمكن أن يحتوي كل معالج معالج Rocket / BOOM على ما يصل إلى أربعة معجلات RoCC تمت إضافتها عبر التكوين ، وهناك أيضًا أمثلة للتنفيذ:
Configs.scala:
class WithRoccExample extends Config((site, here, up) => { case BuildRoCC => List( (p: Parameters) => { val accumulator = LazyModule(new AccumulatorExample(OpcodeSet.custom0, n = 4)(p)) accumulator }, (p: Parameters) => { val translator = LazyModule(new TranslatorExample(OpcodeSet.custom1)(p)) translator }, (p: Parameters) => { val counter = LazyModule(new CharacterCountExample(OpcodeSet.custom2)(p)) counter }) })
تطبيق المطابق موجود في ملف LazyRoCC.scala
.
يمثل تطبيق التسريع فئتين LazyRoCC
بالفعل من وحدة تحكم الذاكرة: أحدهما في هذه الحالة موروث من LazyRoCC
، والآخر من LazyRoCCModuleImp
. تحتوي الفئة الثانية على منفذ io
من النوع RoCCIO
، والذي يحتوي على منفذ طلب cmd
، ومنفذ استجابة resp
، ومنفذ mem للوصول إلى ذاكرة التخزين المؤقت L1D ، ومخرجات busy
ومقاطعة ، ومدخلات exception
. يوجد أيضًا منفذ للمشي بجدول الصفحات ووحدات FPU لا يبدو أننا بحاجة إليها بعد (على أي حال ، لا يوجد حساب حقيقي في eBPF). حتى الآن أريد أن أحاول القيام بشيء ما بهذا النهج ، لذلك لن أتطرق إلى interrupt
. أيضًا ، كما أفهمها ، هناك واجهة TileLink للوصول إلى الذاكرة غير المخزنة مؤقتًا ، لكن الآن لن أتطرق إليها أيضًا.
منظم الاستعلام
لذلك ، لدينا منفذ للوصول إلى ذاكرة التخزين المؤقت ، ولكن واحد فقط. في الوقت نفسه ، يمكن أن تزيد دالة ، على سبيل المثال ، متغير (والذي ، على الأقل ، يمكن تحويله إلى عملية ذرية واحدة) أو حتى يحولها بطريقة غير تامة عن طريق تحميلها وتحديثها وحفظها. في النهاية ، يمكن أن يقدم تعليمة واحدة عدة طلبات غير مرتبطة. ربما ليست هذه هي أفضل فكرة من حيث الأداء ، ولكن ، من ناحية أخرى ، لماذا لا ، على سبيل المثال ، لا تقوم بتحميل ثلاث كلمات (ربما ، بالفعل ، موجودة بالفعل في ذاكرة التخزين المؤقت) ، بمعالجتها بطريقة ما بالتوازي مع منطق التوافقية (ثم لديك فوز واحد) وحفظ النتيجة. لذلك ، نحن بحاجة إلى نوع من المخططات التي "تحل" بشكل فعال محاولات الوصول الموازي إلى منفذ ذاكرة التخزين المؤقت واحد.
سيكون المنطق كالتالي: في بداية إنشاء تطبيق حقنة فرعية محددة (حقل funct
ذي 7 بت من حيث RoCC) ، يتم إنشاء مثيل لمتسلسل الاستعلام (جعل أحد العناصر يبدو ضارًا جدًا بالنسبة لي ، لأنه ينشئ مجموعة من التبعيات الإضافية بين الطلبات التي لا يمكن تنفيذها في وقت واحد ، و سوف الإهدار Fmax على الأرجح). بعد ذلك ، يتم تسجيل كل "موفر" / "محمل" تم إنشاؤه في برنامج التسلسل. في طابور مباشر ، إذا جاز التعبير. في كل تدبير ، يتم تحديد الطلب الأول في أمر التسجيل - يتم إعطاؤه الإذن في الإجراء التالي . بطبيعة الحال ، يحتاج هذا المنطق إلى تراكبه بشكل صحيح مع الاختبارات (ليس لدي الكثير منهم ، لذلك ، ليس هذا التحقق ، ولكنه الحد الأدنى الضروري لتعيين شيء واضح على الأقل). لقد استخدمت PeekPokeTester
القياسي من مكون رسمي أكثر أو أقل لاختبار تصميمات PeekPokeTester
. وصفتها بالفعل مرة واحدة .
وكانت النتيجة مثل هذا الانتهاك:
class Serializer(isComputing: Bool, next: Bool) { def monotonic(x: Bool): Bool = { val res = WireInit(false.B) val prevRes = RegInit(false.B) prevRes := res && isComputing res := (x || prevRes) && isComputing res } private def noone(bs: Seq[Bool]): Bool = !bs.foldLeft(false.B)(_ || _) private val previousReqs = ArrayBuffer[Bool]() def nextReq(x: Bool): (Bool, Int) = { val enable = monotonic(x) val result = RegInit(false.B) val retired = RegInit(false.B) val doRetire = result && next val thisReq = enable && !retired && !doRetire val reqWon = thisReq && noone(previousReqs) when (isComputing) { when(reqWon) { result := true.B } when(doRetire) { result := false.B retired := true.B } } otherwise { result := false.B retired := false.B } previousReqs += thisReq (result, previousReqs.length - 1) } }
يرجى ملاحظة أنه هنا في عملية إنشاء دائرة رقمية ، يتم تنفيذ رمز Scala بأمان. إذا ألقيت نظرة فاحصة ، يمكنك حتى أن تلاحظ وجود ArrayBuffer
مكدسة فيه أجزاء الدائرة ( Boolean
هي نوع من Scala ، Bool
هو نوع من أزاميل يمثل معدات حية ، وليس بعض منطقية معروفة في وقت التشغيل).
العمل مع L1D ذاكرة التخزين المؤقت
يحدث العمل مع ذاكرة التخزين المؤقت في الغالب من خلال io.mem.req
طلب io.mem.resp
استجابة io.mem.resp
. في الوقت نفسه ، تم تزويد منفذ الطلب بالإشارات التقليدية ready
والسارية: أول ما يخبرك أنه مستعد لقبول الطلب ، والثاني نقول إن الطلب جاهز ولديه بالفعل البنية الصحيحة ، على طول المقدمة ، valid && resp
الطلب مقبولاً. في بعض هذه الواجهات ، هناك شرط "عدم استجابة" الإشارات من لحظة الضبط إلى true
وإلى الحافة الإيجابية اللاحقة valid && resp
(يمكن إنشاء هذا التعبير باستخدام طريقة fire()
للراحة).
يحتوي منفذ استجابة الاستجابة ، بدوره ، على علامة valid
فقط ، وهذه هي مشكلة المعالج في إزاحة الإجابات في دورة واحدة على مدار الساعة: "جاهز دائمًا" عن طريق الافتراض ، وإرجاع fire()
valid
فقط.
كما قلت بالفعل ، لا يمكنك تقديم طلبات عندما يكون الأمر فظيعًا: لا يمكنك كتابة شيء ما ، لا أعرف ماذا ، وقراءة مرة أخرى ما سيتم الكتابة فوقه لاحقًا على أساس القيمة المخصومة أمر غريب إلى حد ما. لكن فئة Serializer
تتفهم هذا بالفعل ، لكننا نعطيها فقط إشارة إلى أن الطلب الحالي قد انتقل بالفعل إلى ذاكرة التخزين المؤقت: next = io.mem.req.fire()
. كل ما يمكن القيام به هو التأكد من أنه في "القارئ" يتم تحديث الإجابة فقط عندما تأتي بالفعل - ليس قبل ذلك وليس لاحقًا. هناك طريقة مريحة holdUnless
. والنتيجة هي التنفيذ التالي تقريبا:
class Constructor extends BpfCircuitConstructor { val serializer = new Serializer(isComputing, io.mem.req.fire()) override def doMemLoad(addr: UInt, tpe: LdStType, valid: Bool): (UInt, Bool) = { val (doReq, thisTag) = serializer.nextReq(valid) when (doReq) { io.mem.req.bits.addr := addr require((1 << io.mem.req.bits.tag.getWidth) > thisTag) io.mem.req.bits.tag := thisTag.U io.mem.req.bits.cmd := M_XRD io.mem.req.bits.typ := (4 | tpe.lgsize).U io.mem.req.bits.data := 0.U io.mem.req.valid := true.B } val doResp = isComputing && serializer.monotonic(doReq && io.mem.req.fire()) && io.mem.resp.valid && io.mem.resp.bits.tag === thisTag.U && io.mem.resp.bits.cmd === M_XRD (io.mem.resp.bits.data holdUnless doResp, serializer.monotonic(doResp)) } override def doMemStore(addr: UInt, tpe: LdStType, data: UInt, valid: Bool): Bool = { val (doReq, thisTag) = serializer.nextReq(valid) when (doReq) { io.mem.req.bits.addr := addr require((1 << io.mem.req.bits.tag.getWidth) > thisTag) io.mem.req.bits.tag := thisTag.U io.mem.req.bits.cmd := M_XWR io.mem.req.bits.typ := (4 | tpe.lgsize).U io.mem.req.bits.data := data io.mem.req.valid := true.B } serializer.monotonic(doReq && io.mem.req.fire()) } override def resolveSymbol(sym: BpfLoader.Symbol): Resolved = sym match { case BpfLoader.Symbol(symName, _, size, ElfConstants.Elf64_Shdr.SHN_COMMON, false) if size <= 8 => RegisterReference(regs.getOrElseUpdate(symName, RegInit(0.U(64.W)))) } }
يتم إنشاء مثيل من هذه الفئة لكل إنشاء فرعي.
ليس كل شيء على الكومة متغير عمومي
حسنًا ، ما هو مثال نموذجي؟ ما هو الأداء الذي أود ضمانه؟ بالطبع ، AFL الأجهزة! يبدو في الإصدار الكلاسيكي مثل هذا:
#include <stdint.h> extern uint8_t *__afl_area_ptr; extern uint64_t prev; void inst_branch(uint64_t tag) { __afl_area_ptr[((prev >> 1) ^ tag) & 0xFFFF] += 1; prev = tag; }
كما ترون ، فإنه يحتوي على تحميل وحفظ أكثر أو أقل منطقية (وبينها زيادة) من بايت واحد من __afl_area_ptr
، لكن هنا يطلب السجل الدور prev
!
هذا هو السبب في أن الواجهة Resolved
مطلوبة: يمكنها إما التفاف عنوان ذاكرة عادي أو أن تكون مرجعًا للتسجيل. في الوقت نفسه ، لا أظن حتى الآن سوى أن أعتبر السجلات العددية التي تبلغ أحجامها 1 أو 2 أو 4 أو 8 بايت ، والتي تتم قراءتها دائمًا عند صفر إزاحة ، لذلك بالنسبة للسجلات ، يمكنك تنفيذ طلب المكالمات بهدوء نسبيًا. في هذه الحالة ، من المفيد جدًا معرفة أنه يجب أولاً طرح الطرح واستخدامه لحساب الفهرس ، ثم إعادة كتابته فقط.
والآن الأجهزة
في مرحلة ما ، حصلنا على مسرع منفصل وأكثر أو أقل من خلال واجهة RoCC. ماذا الان؟ إعادة تنفيذ كل نفس ، ودفع من خلال خط أنابيب المعالج؟ يبدو لي أنه ستكون هناك حاجة إلى عدد أقل من العكازات إذا تم ، بالتوازي مع التعليمات funct
تنشيط funct
الثانوي مع funct
قيمة الأداة المساعدة الصادرة تلقائيًا. من حيث المبدأ ، كان علي أيضًا أن أعذب نفسي لذلك: حتى أنني تعلمت استخدام SignalTap ، لأن تصحيح الأخطاء يكاد يكون أعمى ، وحتى مع إعادة تجميع خمس دقائق بعد أدنى تغيير (باستثناء تغيير bootrom - كل شيء سريع هناك) - هذا كثير جدًا بالفعل.
ونتيجة لذلك ، تم تصحيح وحدة فك ترميز الأوامر وتم "تعديل" خط الأنابيب قليلاً لمراعاة حقيقة أنه بغض النظر عن ما يقوله وحدة فك الترميز عن التعليمات الأصلية ، فإن RoCC الذي تم تنشيطه فجأة لا يعني أنه سيكون هناك كتابة زمن انتقال طويل إلى سجل الإخراج ، كما هو الحال أثناء عملية التقسيم وافتقد ذاكرة التخزين المؤقت للبيانات.
بشكل عام ، وصف التعليمات هو زوج ([نمط للتعرف على تعليمة] ، [مجموعة من القيم لتكوين كتل مسار البيانات من المعالج الأساسي]). على سبيل المثال ، يبدو الوضع default
(تعليمة غير معترف بها) بهذا الشكل (مأخوذ من IDecode.scala
، في سطح IDecode.scala
يبدو ، بصراحة ، قبيح):
def default: List[BitPat] =
... وصفًا نموذجيًا لأحد الامتدادات الموجودة في Rocket core يتم تنفيذه مثل هذا:
class IDecode(implicit val p: Parameters) extends DecodeConstants { val table: Array[(BitPat, List[BitPat])] = Array( BNE-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SNE, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BEQ-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SEQ, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BLT-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SLT, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BLTU-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SLTU, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BGE-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SGE, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N), BGEU-> List(Y,N,N,Y,N,N,Y,Y,N,A2_RS2, A1_RS1, IMM_SB,DW_X, FN_SGEU, N,M_X, MT_X, N,N,N,N,N,N,N,CSR.N,N,N,N,N),
الحقيقة هي أنه في RISC-V (ليس فقط في RocketChip ، ولكن في بنية الأوامر من حيث المبدأ) تقسيم ISA إلى مجموعة فرعية إلزامية I (عمليات عدد صحيح) ، وكذلك M اختياري (عدد صحيح وتقسيم) ، A (ذرات) يتم دعمها بانتظام إلخ
نتيجة لذلك ، الطريقة الأصلية
def decode(inst: UInt, table: Iterable[(BitPat, List[BitPat])]) = { val decoder = DecodeLogic(inst, default, table) val sigs = Seq(legal, fp, rocc, branch, jal, jalr, rxs2, rxs1, scie, sel_alu2, sel_alu1, sel_imm, alu_dw, alu_fn, mem, mem_cmd, mem_type, rfs1, rfs2, rfs3, wfd, mul, div, wxd, csr, fence_i, fence, amo, dp) sigs zip decoder map {case(s,d) => s := d} this }
تم استبداله بـ
نفسه ، ولكن مع وحدة فك ترميز للأجهزة وتوضيح سبب تفعيل rocc def decode(inst: UInt, table: Iterable[(BitPat, List[BitPat])], handlers: Seq[OpcodeHandler]) = { val decoder = DecodeLogic(inst, default, table) val sigs=Seq(legal, fp, rocc_explicit, branch, jal, jalr, rxs2, rxs1, scie, sel_alu2, sel_alu1, sel_imm, alu_dw, alu_fn, mem, mem_cmd, mem_type, rfs1, rfs2, rfs3, wfd, mul, div, wxd, csr, fence_i, fence, amo, dp) sigs zip decoder map {case(s,d) => s := d} if (handlers.isEmpty) { handler_rocc := false.B handler_rocc_funct := 0.U } else { val handlerTable: Seq[(BitPat, List[BitPat])] = handlers.map { case OpcodeHandler(pattern, funct) => pattern -> List(Y, BitPat(funct.U)) } val handlerDecoder = DecodeLogic(inst, List(N, BitPat(0.U)), handlerTable) Seq(handler_rocc, handler_rocc_funct) zip handlerDecoder map { case (s,d) => s:=d } } rocc := rocc_explicit || handler_rocc this }
من التغييرات في خط أنابيب المعالج ، ربما كان الأكثر غير واضحًا ، هذا:
io.rocc.exception := wb_xcpt && csr.io.status.xs.orR io.rocc.cmd.bits.status := csr.io.status io.rocc.cmd.bits.inst := new RoCCInstruction().fromBits(wb_reg_inst) + when (wb_ctrl.handler_rocc) { + io.rocc.cmd.bits.inst.opcode := 0x0b.U // custom0 + io.rocc.cmd.bits.inst.funct := wb_ctrl.handler_rocc_funct + io.rocc.cmd.bits.inst.xd := false.B + io.rocc.cmd.bits.inst.rd := 0.U + } io.rocc.cmd.bits.rs1 := wb_reg_wdata io.rocc.cmd.bits.rs2 := wb_reg_rs2
من الواضح أن هناك حاجة إلى تصحيح بعض معلمات الطلب إلى المعجل: لا تتم كتابة أي استجابة إلى السجل ، funct
مساوية لما عاد وحدة فك الترميز. ولكن هناك تغيير أقل وضوحًا قليلاً: الحقيقة هي أن هذا الأمر لا ينتقل مباشرةً إلى المعجل (أربعة منهم - أي واحد؟) ، لكن إلى الموجه ، لذلك عليك التظاهر بأن الأمر يحتوي على opcode == custom0
(نعم ، العملية ، وهذا هو بالضبط مسرع الصفر!).
تفتيش
في الواقع ، تفترض هذه المقالة استمرارًا في محاولة لرفع هذا النهج إلى مستوى إنتاج أكثر أو أقل. كحد أدنى ، يجب أن تتعلم حفظ واستعادة السياق (حالة سجلات المعالج الثانوي) عند تبديل المهام. في غضون ذلك ، سوف أتحقق من أنه يعمل بطريقة ما في ظروف الاحتباس الحراري:
#include <stdint.h> uint64_t counter; uint64_t funct1(uint64_t x, uint64_t y) { return __builtin_popcountl(x); } uint64_t funct2(uint64_t x, uint64_t y) { return (x + y) * (x - y); } uint64_t instMUL() { counter += 1; *((uint64_t *)0x81005000) = counter; return 0; }
أضف الآن إلى bootrom/sdboot/sd.c
في السطر main
#include "/path/to/freedom-u-sdk/riscv-pk/machine/encoding.h"
write_csr
, custom0
- custom3
. , illegal instruction, , , , . define
- - , «» binutils customX
RocketChip, , , .
sdboot , , .
:
$ /hdd/trosinenko/rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set directories bootrom" builds/zeowaa-e115/sdboot.elf Reading symbols from builds/zeowaa-e115/sdboot.elf...done. Remote debugging using :3333 0x0000000000000000 in ?? () (gdb) x/d 0x81005000 0x81005000: 123 (gdb) set variable $pc=0x10000 (gdb) c Continuing. ^C Program received signal SIGINT, Interrupt. 0x0000000000010488 in crc16_round (data=<optimized out>, crc=<optimized out>) at sd.c:151 151 crc ^= data; (gdb) x/d 0x81005000 0x81005000: 246
funct1 $ /hdd/trosinenko/rocket-tools/bin/riscv32-unknown-elf-gdb -q -ex "target remote :3333" -ex "set directories bootrom" builds/zeowaa-e115/sdboot.elf Reading symbols from builds/zeowaa-e115/sdboot.elf...done. Remote debugging using :3333 0x0000000000010194 in main () at sd.c:247 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); (gdb) set variable $a5=0 (gdb) set variable $pc=0x10194 (gdb) set variable $a4=0xaa (gdb) display/10i $pc-10 1: x/10i $pc-10 0x1018a <main+46>: sw a3,124(a3) 0x1018c <main+48>: addiw a0,a0,1110 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 => 0x10194 <main+56>: 0x2f7778b 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> (gdb) display/x $a5 2: /x $a5 = 0x0 (gdb) si 0x0000000000010198 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); 1: x/10i $pc-10 0x1018e <main+50>: li a0,25 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 0x10194 <main+56>: 0x2f7778b => 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> 0x101a6 <main+74>: li a5,10 2: /x $a5 = 0x4 (gdb) set variable $a4=0xaabc (gdb) set variable $pc=0x10194 (gdb) si 0x0000000000010198 247 CUSTOMX_R_R_R(0, res, 0xabcdef, 0x123456, 1); 1: x/10i $pc-10 0x1018e <main+50>: li a0,25 0x10190 <main+52>: mv a4,s0 0x10192 <main+54>: mv a5,a0 0x10194 <main+56>: 0x2f7778b => 0x10198 <main+60>: mv s0,a5 0x1019a <main+62>: lbu a5,0(a1) 0x1019e <main+66>: addiw a3,a3,-1 0x101a0 <main+68>: mul a2,a2,a5 0x101a4 <main+72>: bnez a3,0x1019a <main+62> 0x101a6 <main+74>: li a5,10 2: /x $a5 = 0x9
شفرة المصدر