Talimat semantik işlevleri eğiticisi

Bu eğitimin hedefleri şunlardır:

  • Anlamsal işlevlerin talimat semantiğini uygulamak için nasıl kullanıldığını öğrenin.
  • Anlamsal işlevlerin ISA kod çözücü açıklamasıyla ilişkisini öğrenin.
  • RiscV RV32I talimatları için semantik fonksiyonları yazma.
  • Küçük bir "Hello World" komutunu çalıştırarak son simülatörü test edin yürütülebilir.

Anlamsal işlevlere genel bakış

MPACT-Sim'deki anlamsal işlev, yan etkilerinin simüle edilmiş durumda görülebilmesi için bir talimatın talimatın yan etkilerinin görünür olması gibi donanım. Simülatörün kodu çözülmüş her talimatın dahili gösterimi bu öğe için anlamsal işlevi çağırmak üzere kullanılan bir çağrılabilir teşekkür ederiz.

Anlamsal bir işlevde void(Instruction *) imzası bulunur, yani bir işaretçiyi Instruction sınıfının bir örneğine yönlendiren ve void değerini döndürür.

Instruction sınıfı şurada tanımlanmıştır: instruction.h

Özellikle ilgilendiğimiz anlamsal fonksiyonlar yazma konusunda Source(int i) ve Destination(int i) yöntem çağrıları.

Kaynak ve hedef işlenen arayüzleri aşağıda gösterilmiştir:

// The source operand interface provides an interface to access input values
// to instructions in a way that is agnostic about the underlying implementation
// of those values (eg., register, fifo, immediate, predicate, etc).
class SourceOperandInterface {
 public:
  // Methods for accessing the nth value element.
  virtual bool AsBool(int index) = 0;
  virtual int8_t AsInt8(int index) = 0;
  virtual uint8_t AsUint8(int index) = 0;
  virtual int16_t AsInt16(int index) = 0;
  virtual uint16_t AsUint16(int) = 0;
  virtual int32_t AsInt32(int index) = 0;
  virtual uint32_t AsUint32(int index) = 0;
  virtual int64_t AsInt64(int index) = 0;
  virtual uint64_t AsUint64(int index) = 0;

  // Return a pointer to the object instance that implements the state in
  // question (or nullptr) if no such object "makes sense". This is used if
  // the object requires additional manipulation - such as a fifo that needs
  // to be pop'ed. If no such manipulation is required, nullptr should be
  // returned.
  virtual std::any GetObject() const = 0;

  // Return the shape of the operand (the number of elements in each dimension).
  // For instance {1} indicates a scalar quantity, whereas {128} indicates an
  // 128 element vector quantity.
  virtual std::vector<int> shape() const = 0;

  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;

  virtual ~SourceOperandInterface() = default;
};
// The destination operand interface is used by instruction semantic functions
// to get a writable DataBuffer associated with a piece of simulated state to
// which the new value can be written, and then used to update the value of
// the piece of state with a given latency.
class DestinationOperandInterface {
 public:
  virtual ~DestinationOperandInterface() = default;
  // Allocates a data buffer with ownership, latency and delay line set up.
  virtual DataBuffer *AllocateDataBuffer() = 0;
  // Takes an existing data buffer, and initializes it for the destination
  // as if AllocateDataBuffer had been called.
  virtual void InitializeDataBuffer(DataBuffer *db) = 0;
  // Allocates and initializes data buffer as if AllocateDataBuffer had been
  // called, but also copies in the value from the current value of the
  // destination.
  virtual DataBuffer *CopyDataBuffer() = 0;
  // Returns the latency associated with the destination operand.
  virtual int latency() const = 0;
  // Return a pointer to the object instance that implmements the state in
  // question (or nullptr if no such object "makes sense").
  virtual std::any GetObject() const = 0;
  // Returns the order of the destination operand (size in each dimension).
  virtual std::vector<int> shape() const = 0;
  // Return a string representation of the operand suitable for display in
  // disassembly.
  virtual std::string AsString() const = 0;
};

Normal 3 işleneni için semantik fonksiyon yazmanın temel yolu 32 bit add talimatı gibi bir talimat şöyledir:

void MyAddFunction(Instruction *inst) {
  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);
  uint32_t c = a + b;
  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, c);
  db->Submit();
}

Bu fonksiyonun parçalarını yakından inceleyelim. Belgenin ilk iki satırı, işlev gövdesi, kaynak işlem göreleri 0 ve 1'den okur. AsUint32(0) görüşmesi temel verileri bir uint32_t dizisi olarak yorumlar ve 0. öğesine dokunun. Bu durum, temel kaydın veya değerin değeri olup olmadığını gösterir. Kaynak işlenenin boyutu (öğe cinsinden) elde edilen ve bir vektör döndüren shape() kaynak işlem gören yönteminden elde edilmiştir her bir boyuttaki öğe sayısını içerir. Bu yöntem {1} değerini döndürür skaler için, 16 öğeli vektör için {16} ve 4x4 dizi için {4, 4}.

  uint32_t a = inst->Source(0)->AsUint32(0);
  uint32_t b = inst->Source(1)->AsUint32(0);

Ardından a + b değeri, c adlı geçici bir uint32_t atanır.

Sonraki satırda daha fazla açıklama gerekebilir:

  DataBuffer *db = inst->Destination(0)->AllocateDataBuffer();

DataBuffer, değerleri şurada depolamak için kullanılan, referans sayılan bir nesnedir: kayıtlar gibi simüle edilmiş durumlar. Nispeten fazla yazılmamış olsa da ayrıldığı nesneye göre boyut belirler. Bu durumda, bu boyut sizeof(uint32_t) Bu ifade, bu hedef işlenenin hedefi olan hedef - bu örnekte 32 bit tam sayı kaydı. DataBuffer ayrıca gecikme süresini ekleyebilirsiniz. Bu, talimat sırasında belirtilir çözer.

Sonraki satır, veri arabellek örneğini bir uint32_t dizisi ve c içinde depolanan değeri 0. öğeye yazar.

  db->Set<uint32_t>(0, c);

Son olarak, son ifade veri tamponu, etiketinden sonra hedef makine durumunun (bu örnekte bir kayıt) talimatın kodu çözüldüğünde ayarlanan talimatın gecikmesi ve hedef işlenen vektörü doldurulur.

Bu, makul ölçüde kısa bir fonksiyon olsa da birtakım standartlara Talimat uygulanırken yinelenen bir kod görürsünüz. Ayrıca, talimatın gerçek anlamını da gizleyebilir. Siparişte çoğu talimat için anlamsal fonksiyonları yazmayı daha da basitleştirmek, şurada tanımlanmış bir dizi şablonlu yardımcı işlevi vardır: instruction_helpers.h Bu yardımcılar bir, iki veya üçlü talimatlar için ortak metni gizler. kaynak işlenenleri ve tek bir hedef işleneni. Bunlara göz atalım işlenen yardımcı işlevler:

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result and
// each of the two source operands.
template <typename Result, typename Argument1, typename Argument2>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument1, Argument2)> operation) {
  Argument1 lhs = generic::GetInstructionSource<Argument1>(instruction, 0);
  Argument2 rhs = generic::GetInstructionSource<Argument2>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version supports different types for the result
// and the operands, but the two source operands must have the same type.
template <typename Result, typename Argument>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Argument, Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Argument rhs = generic::GetInstructionSource<Argument>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// two operand instruction semantic functions. It reads two source operands
// and applies the function argument to them, storing the result to the
// destination operand. This version requires both result and source operands
// to have the same type.
template <typename Result>
inline void BinaryOp(Instruction *instruction,
                     std::function<Result(Result, Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result rhs = generic::GetInstructionSource<Result>(instruction, 1);
  Result dest_value = operation(lhs, rhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

Aşağıdaki gibi bir ifade kullanmak yerine şunu fark edeceksiniz:

  uint32_t a = inst->Source(0)->AsUint32(0);

Yardımcı işlev şunları kullanır:

generic::GetInstructionSource<Argument>(instruction, 0);

GetInstructionSource, bir şablondan oluşan bir yardımcı işlev ailesidir. Talimat kaynağına şablonlu erişim yöntemleri sağlamak için kullanılır işlenenler. Bunlar olmadan talimat yardımcı işlevinin her biri doğru şekilde kaynak işlenene erişmek için her tür için özelleştirilen As<int type>() işlevi. Bu şablonların tanımlarını işlev instruction.h

Gördüğünüz gibi kaynağın işlem gören türleri, hedefin her zaman hedef ile veya hepsinin farklı olup olmadığı gibi sorular sorabilirsiniz. Her sürümü işlev, talimat örneğine bir işaretçi ve çağrılabilir (lambda işlevlerini içerir). Bu, artık add yeniden yazabileceğimiz anlamına gelir. aşağıdaki anlamsal işleve sahiptir:

void MyAddFunction(Instruction *inst) {
  generic::BinaryOp<uint32_t>(inst,
                              [](uint32_t a, uint32_t b) { return a + b; });
}

Derlemede bazel build -c opt ve copts = ["-O3"] ile derlendiğinde hiçbir ek yük olmadan tamamen satır içinde olmalıdır ve bize kısa ve öz bir metin olmalıdır.

Bahsedildiği gibi, tekli, ikili ve üçlü skaler için yardımcı işlevler vardır ve vektör eşdeğerlerini içerir. Bu öğeler ayrıca oluşturmaya ilişkin talimatları uygulayın.


İlk derleme

Dizini riscv_semantic_functions olarak değiştirmediyseniz, bunu yapın. . Daha sonra projeyi aşağıdaki gibi derleyin. Bu derleme başarılı olacaktır.

$  bazel build :riscv32i
...<snip>...

Hiç dosya oluşturulmadığından bu yalnızca bir prova her şeyin yolunda olduğundan emin olun.


Üç işlenen ALU talimatı ekleme

Şimdi bazı genel, 3 işlenen ALU değişkeni için anlamsal fonksiyonlar bakın. rv32i_instructions.cc dosyasını açın ve İlerledikçe rv32i_instructions.h dosyasına eksik tanımlar eklenir.

Ekleyeceğimiz talimatlar şunlardır:

  • add - 32 bit tam sayı ekle.
  • and - 32 bit bit tabanlı ve.
  • or - 32 bit bit tabanlı veya
  • sll - 32 bit mantıksal kaydırma sola.
  • sltu - 32 bitlik imzasız kümeden küçük.
  • sra - 32 bit aritmetik sağa kaydırma.
  • srl - 32 bit mantıksal sağ kaydırma.
  • sub - 32 bit tamsayı çıkarma işlemi.
  • xor - 32 bit bit tabanlı Xor.

Önceki eğiticileri tamamladıysanız, o ana kadar Kayıt-kayıt talimatları ile kayıt anında talimatları arasındaki kod çözücüye benzer. Anlamsal işlevler söz konusu olduğunda artık buna ihtiyacımız yok. İşlem gören arayüzler, işlenen değeri işlemden hangi düzeyde okuyacak? açısından çok daha iyidir. Semantik fonksiyon, temel kaynak işleneninin gerçekten ne olduğunu belirlemenizi sağlar.

sra hariç yukarıdaki talimatların tümü, 32 bit imzalanmamış değerler; bunlar için BinaryOp şablon işlevini kullanabiliriz tek şablon türü bağımsız değişkeniyle ele aldığımızı unutmayın. fonksiyon gövdelerini buna göre rv32i_instructions.cc içinde değiştirebilirsiniz. Yalnızca düşük olan 5 kayma talimatlarına ait ikinci işlenenin bitleri kaydırma için kullanılır tutar. Aksi takdirde, tüm işlemler src0 op src1 biçiminde olur:

  • add: a + b
  • and: a & b
  • or: a | b
  • sll: a << (b & 0x1f)
  • sltu: (a < b) ? 1 : 0
  • srl: a >> (b & 0x1f)
  • sub: a - b
  • xor: a ^ b

sra için üç bağımsız değişken olan BinaryOp şablonunu kullanacağız. Her bir şablonunda, ilk tür bağımsız değişken, sonuç türüdür: uint32_t. İkincisi, kaynak işleneni 0'ın türü (bu örnekte int32_t, sonuncusu ise türdür) kaynak işlem gören 1'inki (bu örnekte uint32_t). Bu, sra gövdesinin anlamsal fonksiyon:

  generic::BinaryOp<uint32_t, int32_t, uint32_t>(
      instruction, [](int32_t a, uint32_t b) { return a >> (b & 0x1f); });

Devam edin ve değişiklikleri yapın ve geliştirin. Çalışmalarınızı rv32i_instructions.cc olmalıdır.


İki işlenen ALU talimatı ekleme

Yalnızca iki tane 2 işlemli ALU talimatı vardır: lui ve auipc. Önceki önceden kaydırılan kaynak işleneni doğrudan hedefe kopyalar. İkincisi talimatı adresine yazmadan hemen önce seçeceğiz. Talimat adresine address() yönteminden erişilebilir (Instruction nesnesi)

Yalnızca tek bir kaynak işleneni olduğu için bunun yerine BinaryOp kullanamayız UnaryOp kullanmamız gerekiyor. Hem kaynağı hem de dönüşüm değerini uint32_t olarak hedef işlenenleri, tek bağımsız değişken şablonunu kullanabiliriz sürümünü değil.

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version supports the result and argument having
// different types.
template <typename Result, typename Argument>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Argument)> operation) {
  Argument lhs = generic::GetInstructionSource<Argument>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

// This is a templated helper function used to factor out common code in
// single operand instruction semantic functions. It reads one source operand
// and applies the function argument to it, storing the result to the
// destination operand. This version requires that the result and argument have
// the same type.
template <typename Result>
inline void UnaryOp(Instruction *instruction,
                    std::function<Result(Result)> operation) {
  Result lhs = generic::GetInstructionSource<Result>(instruction, 0);
  Result dest_value = operation(lhs);
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->SetSubmit<Result>(0, dest_value);
}

lui için anlamsal işlevin gövdesi olabildiğince önemsizdir. sadece kaynağı döndürür. auipc için semantik işlev küçük yaştaki bir kişiyi tanıtıyor Instruction içinde address() yöntemine erişmeniz gerektiğinden kullanır. Cevap, lambda yakalamasına instruction eklemektir. lambda işlev gövdesinde kullanılabilir. Önceki gibi [](uint32_t a) { ... } yerine lambda [instruction](uint32_t a) { ... } yazılmalıdır. instruction artık lambda gövdesinde kullanılabilir.

Devam edin ve değişiklikleri yapın ve geliştirin. Çalışmalarınızı rv32i_instructions.cc olmalıdır.


Kontrol akışı değişikliği talimatları ekleyin

Uygulamanız gereken kontrol akışı değişikliği talimatları, koşullu dal talimatlarına (bir koşula bağlı olarak karşılaştırmanın gerçek kabul edilir) ve atlama ve bağlantı talimatlarının karşılaştırması. işlev çağrılarını uygulama (-ve-link, bağlantının ayarlanmasıyla kaldırılır) sıfıra kaydedilerek bu yazmaların hiç işlem yapılmamasına yol açar).

Koşullu dal talimatları ekleme

Dal talimatı için yardımcı bir işlev olmadığından iki seçenek vardır. Anlamsal fonksiyonları sıfırdan yazın veya yerel bir yardımcı fonksiyon yazın. 6 şube talimatını uygulamamız gerektiğinden ikincisi şuna değer görünüyor: çaba sarf etmeniz gerekir. Bunu yapmadan önce dal (dalga) uygulamasına bakalım. komut semantik işlevini sıfırdan oluşturmayı öğreneceğiz.

void MyConditionalBranchGreaterEqual(Instruction *instruction) {
  int32_t a = generic::GetInstructionSource<int32_t>(instruction, 0);
  int32_t b = generic::GetInstructionSource<int32_t>(instruction, 1);
  if (a >= b) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0,m target);
    db->Submit();
  }
}

Şube talimatlarında değişiklik gösteren tek şey, durumunu ve veri türlerini (imzalı ve işaretsiz 32 bit int ile karşılaştırıldığında) kaynak işlenenlerini gösterir. Bu, kaynak işlenenlerini gösterir. Yardımcı işlevin Instruction öğesini alması gerekir örnek ve bool döndüren std::function gibi çağrılabilir bir nesne ekleyebilirsiniz. Yardımcı işlev şöyle görünür:

template <typename OperandType>
static inline void BranchConditional(
    Instruction *instruction,
    std::function<bool(OperandType, OperandType)> cond) {
  OperandType a = generic::GetInstructionSource<OperandType>(instruction, 0);
  OperandType b = generic::GetInstructionSource<OperandType>(instruction, 1);
  if (cond(a, b)) {
    uint32_t offset = generic::GetInstructionSource<uint32_t>(instruction, 2);
    uint32_t target = offset + instruction->address();
    DataBuffer *db = instruction->Destination(0)->AllocateDataBuffer();
    db->Set<uint32_t>(0, target);
    db->Submit();
  }
}

Şimdi bge (işaretli dal büyük veya eşit) anlamsal işlevini yazabiliriz yön:

void RV32IBge(Instruction *instruction) {
  BranchConditional<int32_t>(instruction,
                             [](int32_t a, int32_t b) { return a >= b; });
}

Diğer dal talimatları aşağıdaki gibidir:

  • Beq - dal eşit.
  • Bgeu - daha büyük veya eşit (imzasız).
  • Blt - dal küçüktür (imzalı).
  • Bltu - şundan küçük (imzasız).
  • Bne - dal eşit değil.

Devam edin ve bu anlamsal işlevleri uygulamak için gerekli değişiklikleri yapın ve yeniden oluşturabilirsiniz. Çalışmalarınızı rv32i_instructions.cc olmalıdır.

Atlama ve bağlantı için yardımcı işlev yazmanın bir anlamı yoktur bu yüzden bunları sıfırdan yazmamız gerekecek. İlk olarak ve talimatların anlamlarına bakıyorlar.

jal talimatı, kaynak işlem gören 0'dan bir ofset alır ve bunu atlama hedefini hesaplamak için geçerli pc'ye (talimat adresi) gidin. Atlama hedefi hedef işlem gören 0'a yazılır. İade adresi, bir sonraki adımdır. Hesaplamak için mevcut talimatının boyutuna uymasını sağlar. İade adresi, hedef işlem gören 1. Talimat nesne işaretçisini yakalarsınız.

jalr talimatı, kaynak işlem göreni 0 olarak bir temel kayıt ve ofset olarak alır olarak çalışır ve atlama hedefini hesaplamak için bunları bir araya getirir. Aksi takdirde, jal talimatıyla aynı olur.

Talimat semantiğinin bu açıklamalarına dayanarak, iki anlamı yazın geliştirmeyi öğreneceksiniz. Çalışmalarınızı rv32i_instructions.cc olmalıdır.


Anı depolama talimatları ekleyin

Uygulamamız gereken üç mağaza talimatı var: mağaza baytı (sb), mağaza yarım kelimesi (sh) ve mağaza kelimesi (sw). Mağaza talimatları kullanıcı tarafından sağlanmaması açısından, şu ana kadar yerel işlemci durumuna yazma. Bunun yerine bir sistem kaynağına yazarlar ve ana bellek. MPACT-Sim, belleği bir komut işleneni olarak işlemez. Bu nedenle, belleğe erişiminin başka bir yöntemle gerçekleştirilmesi gerekir.

Cevap, MPACT-Sim ArchState nesnesine bellek erişim yöntemleri eklemektir. veya daha doğru bir şekilde, ArchState değişkeninden türetilen yeni bir RiscV durum nesnesi oluşturun eklenip eklenmediğini kontrol edin. ArchState nesnesi, aşağıdakiler gibi temel kaynakları yönetir: kayıtları ve diğer durum nesnelerini kapsar. Aynı zamanda hedef işlenen veri arabelleklerini, bu tamponlara tekrar yazılıncaya kadar olduğunu unutmayın. Çoğu talimat, bilgisi olmadan da bellek işlemleri ve belirli bir sistemden kaynaklanan talimatların bu durum nesnesinde bulunması için işlev gerekir.

fence talimatının anlamsal işlevine örnek olarak rv32i_instructions.cc dilinde zaten uygulanmıştır. fence talimat, belirli bellek işlemleri etkin olana kadar talimat sorununu tamamlandı. Talimatlar arasındaki bellek düzenini garanti etmek için kullanılır koordine etmelerini sağlayan bir iş yönetimi yazılımıdır.

// Fence.
void RV32IFence(Instruction *instruction) {
  uint32_t bits = instruction->Source(0)->AsUint32(0);
  int fm = (bits >> 8) & 0xf;
  int predecessor = (bits >> 4) & 0xf;
  int successor = bits & 0xf;
  auto *state = static_cast<RiscVState *>(instruction->state());
  state->Fence(instruction, fm, predecessor, successor);
}

fence talimatının anlamsal işlevinin temel kısmı son iki öğedir satırlarda ilerleyin. İlk olarak durum nesnesi, Instruction içindeki bir yöntem kullanılarak getirilir. sınıfını ve downcast<> değerini RiscV'ye özgü türetilmiş sınıfa ekler. Ardından Fence RiscVState sınıfının yöntemi çağrılır.

Mağaza talimatları da benzer şekilde çalışır. İlk olarak, bellek erişimi taban ve ofset talimatı kaynak işlenenlerinden hesaplanır. depolanacak değer, bir sonraki kaynak işleneninden getirilir. Ardından, RiscV durum nesnesi, state() yöntem çağrısı ile ve static_cast<> ve uygun yöntem çağrılır.

RiscVState nesnesi StoreMemory yöntemi nispeten basittir ancak bir dikkat etmemiz gereken birkaç nokta var:

  void StoreMemory(const Instruction *inst, uint64_t address, DataBuffer *db);

Gördüğümüz gibi, bu yöntemde mağazanın işaretçisi ve talimat, mağaza adresi ve DataBuffer işaretçisi mağaza verilerini içeren bir örnek teşkil eder. Boyuta gerek olmadığına dikkat edin, DataBuffer örneğinin kendisi bir size() yöntemi içerir. Ancak, için kullanılabilecek, talimat tarafından erişilebilen hedef işlem göreni uygun boyutta bir DataBuffer örneği ayırma. Bunun yerine db_factory() yönteminden elde edilmiş bir DataBuffer fabrikası kullanın: Instruction örneği. Fabrikanın Allocate(int size) yöntemi var. gereken boyutta bir DataBuffer örneği döndürür. Bir örnekle açıklayalım yarım kelimelik bir mağaza için DataBuffer örneği ayırmak amacıyla bunun nasıl kullanılacağını (auto öğesinin, türü sağ taraftan tespit eden bir C++ özelliği olduğuna dikkat edin göz atabilirsiniz):

  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(uint16_t));

DataBuffer örneğini aldıktan sonra, bu örneğe her zamanki gibi yazabiliriz:

  db->Set<uint16_t>(0, value);

Daha sonra, bunu bellek deposu arayüzüne iletin:

  state->StoreMemory(instruction, address, db);

Henüz işimiz bitmedi. DataBuffer örneği referans sayılır. Bu makul bir şekilde Submit yöntemi tarafından anlaşılıp işlenir; dolayısıyla, durumu olabildiğince basit hale getirmektir. Ancak StoreMemory, çok iyi olur. Çalışırken DataBuffer örneği IncRef ve tamamlandığında DecRef. Ancak, anlamsal fonksiyon DecRef kendine ait referansa sahip olduğu için hiçbir zaman tekrar hak talebinde bulunulmaz. Dolayısıyla, son satırda olması için:

  db->DecRef();

Üç mağaza işlevi vardır ve tek fark mağazanın hafızaya erişimle ilgiliydi. Bu, bölge sakini için harika bir fırsat şablonlu yardımcı işlevi görür. Mağaza işlevindeki tek fark, mağaza değerinin türünü belirtecek şekilde, şablonun bir bağımsız değişken olarak bunu içermesi gerekir. Bunun dışında, yalnızca Instruction örneğinin aktarılması gerekir:

template <typename ValueType>
inline void StoreValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto value = generic::GetInstructionSource<ValueType>(instruction, 2);
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->Set<ValueType>(0, value);
  state->StoreMemory(instruction, address, db);
  db->DecRef();
}

Devam edin ve mağaza semantik fonksiyonlarını tamamlayıp derlemeyi tamamlayın. Web sitemiz g.co/newsinitiative/labs üzerinden karşı çalışma rv32i_instructions.cc olmalıdır.


Bellek yükü talimatları ekleyin

Uygulanması gereken yükleme talimatları şunlardır:

  • lb - bayt yükleyin, bir kelimeye oturum açın.
  • lbu - baytı imzalanmamış şekilde yükleyin, sıfır olarak bir kelimeye genişletin.
  • lh - yarım kelime yükleyin, oturum açarak bir kelimeye genişletin.
  • lhu - yarım kelimelik imzasız yükleyin, sıfır kelimeden oluşan bir kelime ekleyin.
  • lw - kelimeyi yükle.

Yükleme talimatları, modellememiz gereken en karmaşık talimatlardır. inceleyebilirsiniz. Mağaza talimatlarına benzerler, RiscVState nesnesine erişir ancak her yükleme işleminin karmaşıklığını talimatlar iki ayrı anlamsal işleve bölünür. İlki etkili adresi hesaplaması açısından mağaza talimatlarına benzer ve bellek erişimini başlatır. İkincisi ise hafıza ve bellek verilerini kayıt hedefine yazar işlenen.

RiscVState içindeki LoadMemory yöntemi beyanını inceleyerek başlayalım:

  void LoadMemory(const Instruction *inst, uint64_t address, DataBuffer *db,
                  Instruction *child_inst, ReferenceCount *context);

StoreMemory yöntemine kıyasla LoadMemory, iki ek yöntem alır parametreleri: Instruction örneğine işaretçi ve referans context nesne sayıldı. İlki, çocuklara eğitim veren child talimatıdır. kayıt geri yazma işlemini uygular (ISA kod çözücü eğiticisinde açıklanmıştır). Google mevcut Instruction örneğinde child() yöntemi kullanılarak erişilir. İkincisi, başlangıçtaki ReferenceCount; bu durumda şunları yapacak bir DataBuffer örneğini depolayacaktır: emin olun. Bağlam nesnesi, Instruction nesnesinde context() yöntemini kullanmak (çoğu talimat için nullptr olarak ayarlanır).

RiscV bellek yüklemeleri için bağlam nesnesi aşağıdaki struct olarak tanımlanır:

// A simple load context class for convenience.
struct LoadContext : public generic::ReferenceCount {
  explicit LoadContext(DataBuffer *vdb) : value_db(vdb) {}
  ~LoadContext() override {
    if (value_db != nullptr) value_db->DecRef();
  }

  // Override the base class method so that the data buffer can be DecRef'ed
  // when the context object is recycled.
  void OnRefCountIsZero() override {
    if (value_db != nullptr) value_db->DecRef();
    value_db = nullptr;
    // Call the base class method.
    generic::ReferenceCount::OnRefCountIsZero();
  }
  // Data buffers for the value loaded from memory (byte, half, word, etc.).
  DataBuffer *value_db = nullptr;
};

Veri boyutu (bayt, veri boyutu) hariç olmak üzere yükleme talimatlarının yarım kelime ve kelime) ve yüklenen değerin işaretli olup olmadığı. İlgili içeriği oluşturmak için kullanılan yalnızca child talimatını dikkate alır. Şimdi bir şablon oluşturalım ana yükleme talimatlarının yardımcı işlevi. Bu, mağaza talimatı vardır ancak değer almak için kaynak işlenene erişemez. bir bağlam nesnesi oluşturur.

template <typename ValueType>
inline void LoadValue(Instruction *instruction) {
  auto base = generic::GetInstructionSource<uint32_t>(instruction, 0);
  auto offset = generic::GetInstructionSource<uint32_t>(instruction, 1);
  uint32_t address = base + offset;
  auto *state = down_cast<RiscVState *>(instruction->state());
  auto *db = state->db_factory()->Allocate(sizeof(ValueType));
  db->set_latency(0);
  auto *context = new riscv::LoadContext(db);
  state->LoadMemory(instruction, address, db, instruction->child(), context);
  context->DecRef();
}

Gördüğünüz gibi temel fark, ayrılan DataBuffer örneğinin hem LoadMemory çağrısına parametre olarak iletilir hem de LoadContext nesne.

child talimatı anlamsal işlevlerinin tümü çok benzerdir. İlk olarak, LoadContext, Instruction yöntemi (context()) çağırılarak elde edilir ve LoadContext * öğesine statik olarak yayınlanır. İkincisi, değer (verilere göre, type), load-data DataBuffer örneğinden okunur. Üçüncüsü, yeni bir DataBuffer örneği, hedef işlenenden ayrıldı. Son olarak, yüklenen değer yeni DataBuffer örneğine yazılır ve Submit eklenir. Yine de şablonlu bir yardımcı işlev iyi bir fikirdir:

template <typename ValueType>
inline void LoadValueChild(Instruction *instruction) {
  auto *context = down_cast<riscv::LoadContext *>(instruction->context());
  uint32_t value = static_cast<uint32_t>(context->value_db->Get<ValueType>(0));
  auto *db = instruction->Destination(0)->AllocateDataBuffer();
  db->Set<uint32_t>(0, value);
  db->Submit();
}

Şu son yardımcı işlevleri ve anlamsal işlevleri devam edin. Öde her yardımcı işlev için şablonda kullandığınız veri türüne dikkat edin. çağrısına göre belirlenir ve yüklemenin boyutuna ve imzalı/imzasız yapısına teşekkür ederiz.

Çalışmalarınızı rv32i_instructions.cc olmalıdır.


Nihai simülatörü derleme ve çalıştırma

Zor sözleri tamamladığına göre artık nihai simülatörü geliştirebiliriz. İlgili içeriği oluşturmak için kullanılan bu eğiticilerdeki tüm çalışmaları birbirine bağlayan en üst düzey C++ kitaplıkları other/ adresinde bulunuyor. Bu kodu ayrıntılı olarak incelemenize gerek yoktur. Biz ileri düzey bir eğitimde bu konuyu ziyaret edecektir.

Çalışma dizininizi other/ olarak değiştirin ve derleyin. Uygulama, hatalar.

$ cd ../other
$ bazel build :rv32i_sim

Bu dizinde basit bir "hello world" (merhaba dünya) dosyası vardır. programla hello_rv32i.elf Simülasyon aracını bu dosyada çalıştırmak ve sonuçları görmek için:

$ bazel run :rv32i_sim -- other/hello_rv32i.elf

Aşağıdakilere benzer bir sonuç görmeniz gerekir:

INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.203s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim other/hello_rv32i.elf
Starting simulation
Hello World
Simulation done
$

Simülatör, bazel run :rv32i_sim -- -i other/hello_rv32i.elf komutu kullanılarak etkileşimli modda da çalıştırılabilir. Böylece karşınıza basit bir komut kabuğundaki gibidir. Kullanılabilir komutları görmek için isteme help yazın.

$ bazel run :rv32i_sim -- -i other/hello_rv32i.elf
INFO: Analyzed target //other:rv32i_sim (0 packages loaded, 0 targets configured).
INFO: Found 1 target...
Target //other:rv32i_sim up-to-date:
  bazel-bin/other/rv32i_sim
INFO: Elapsed time: 0.180s, Critical Path: 0.00s
INFO: 1 process: 1 internal.
INFO: Build completed successfully, 1 total action
INFO: Running command line: bazel-bin/other/rv32i_sim -i other/hello_rv32i.elf
_start:
80000000   addi           ra, 0, 0
[0] > help


    quit                           - exit command shell.
    core [N]                       - direct subsequent commands to core N
                                     (default: 0).
    run                            - run program from current pc until a
                                     breakpoint or exit. Wait until halted.
    run free                       - run program in background from current pc
                                     until breakpoint or exit.
    wait                           - wait for any free run to complete.
    step [N]                       - step [N] instructions (default: 1).
    halt                           - halt a running program.
    reg get NAME [FORMAT]          - get the value or register NAME.
    reg NAME [FORMAT]              - get the value of register NAME.
    reg set NAME VALUE             - set register NAME to VALUE.
    reg set NAME SYMBOL            - set register NAME to value of SYMBOL.
    mem get VALUE [FORMAT]         - get memory from location VALUE according to
                                     format. The format is a letter (o, d, u, x,
                                     or X) followed by width (8, 16, 32, 64).
                                     The default format is x32.
    mem get SYMBOL [FORMAT]        - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem SYMBOL [FORMAT]            - get memory from location SYMBOL and format
                                     according to FORMAT (see above).
    mem set VALUE [FORMAT] VALUE   - set memory at location VALUE(1) to VALUE(2)
                                     according to FORMAT. Default format is x32.
    mem set SYMBOL [FORMAT] VALUE  - set memory at location SYMBOL to VALUE
                                     according to FORMAT. Default format is x32.
    break set VALUE                - set breakpoint at address VALUE.
    break set SYMBOL               - set breakpoint at value of SYMBOL.
    break VALUE                    - set breakpoint at address VALUE.
    break SYMBOL                   - set breakpoint at value of SYMBOL.
    break clear VALUE              - clear breakpoint at address VALUE.
    break clear SYMBOL             - clear breakpoint at value of SYMBOL.
    break clear all                - remove all breakpoints.
    help                           - display this message.

_start:
80000000   addi           ra, 0, 0
[0] >

Bu eğitimin sonuna geldik. Umarız bu bilgiler işinize yaramıştır.