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ı veyasll
- 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ı talimatları ekleyin
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.