Seperti yang dijanjikan , kami terus berbicara tentang pengembangan
prosesor Elbrus . Artikel ini bersifat teknis. Informasi yang diberikan dalam artikel ini bukan dokumentasi resmi, karena diperoleh selama studi tentang Elbrus seperti kotak hitam. Tetapi tentu akan menarik untuk pemahaman yang lebih baik tentang arsitektur Elbrus, karena meskipun kami memiliki dokumentasi resmi, banyak detail menjadi jelas hanya setelah percobaan yang panjang, ketika
Embox berhasil.
Ingatlah bahwa pada
artikel sebelumnya kita berbicara tentang boot sistem dasar dan driver port serial. Embox dimulai, tetapi untuk kemajuan lebih lanjut kami membutuhkan interupsi, timer sistem dan, tentu saja, saya ingin memasukkan beberapa set unit test, dan untuk ini kita perlu setjmp. Artikel ini akan fokus pada register, tumpukan, dan detail teknis lainnya yang diperlukan untuk mengimplementasikan semua hal ini.
Mari kita mulai dengan pengantar singkat tentang arsitektur, yang merupakan informasi minimum yang diperlukan untuk memahami apa yang akan dibahas nanti. Di masa mendatang, kami akan merujuk pada informasi dari bagian ini.
Pendahuluan singkat: Tumpukan
Ada tiga tumpukan di Elbrus:
- Tumpukan prosedur (PS)
- Prosedur Stack Rantai (PCS)
- Tumpukan pengguna (kami)
Mari kita analisa mereka secara lebih detail. Alamat pada gambar bersyarat, menunjukkan ke arah mana gerakan diarahkan - dari alamat yang lebih besar ke alamat yang lebih kecil atau sebaliknya.

Susunan prosedur (PS) dimaksudkan untuk data yang dialokasikan untuk register "operasional".
Misalnya, itu mungkin argumen fungsi, dalam arsitektur "biasa", konsep ini paling dekat dengan register tujuan umum. Tidak seperti arsitektur prosesor "biasa", di E2K, register yang digunakan dalam fungsi ditumpuk pada tumpukan yang terpisah.
Stack of Binding Information (PCS) dirancang untuk menempatkan informasi tentang prosedur (panggilan) sebelumnya dan digunakan ketika kembali. Data pada alamat kembali, serta dalam kasus register, ditempatkan di tempat yang terpisah. Oleh karena itu, tumpukan promosi (misalnya, untuk keluar dengan pengecualian dalam C ++) adalah proses yang lebih memakan waktu daripada di arsitektur "biasa". Di sisi lain, ini menghilangkan masalah stack overflow.
Kedua tumpukan ini (PS dan PCS) dicirikan oleh alamat dasar, ukuran, dan offset saat ini. Parameter-parameter ini diatur dalam register PSP dan PCSP, mereka 128-bit dan dalam assembler Anda perlu merujuk ke bidang tertentu (misalnya, tinggi atau rendah). Selain itu, fungsi tumpukan sangat terkait dengan konsep file register, lebih dari itu di bawah ini. Interaksi dengan file terjadi melalui mekanisme register pemompaan / pertukaran. Peran aktif dalam mekanisme ini dimainkan oleh apa yang disebut "pointer perangkat keras ke atas tumpukan" masing-masing prosedural atau tumpukan informasi yang mengikat. Tentang itu juga di bawah ini. Adalah penting bahwa pada setiap titik waktu data dari tumpukan ini baik dalam RAM atau dalam file register.
Perlu juga dicatat bahwa tumpukan ini (tumpukan prosedural dan tumpukan informasi yang mengikat) tumbuh. Kami menemukan ini ketika kami menerapkan context_switch.
Tumpukan pengguna juga diberi alamat dan ukuran dasar. Pointer saat ini ada di register USD.lo. Pada intinya, itu adalah tumpukan klasik yang tumbuh turun. Hanya, tidak seperti arsitektur "biasa", informasi dari tumpukan lain (register dan alamat pengirim) tidak cocok di sana.
Satu non-standar, menurut pendapat saya, persyaratan untuk batas dan ukuran tumpukan adalah perataan 4K, dengan alamat dasar tumpukan dan ukurannya harus disejajarkan dengan 4K. Di arsitektur lain, saya belum menemukan batasan seperti itu. Kami menemukan detail ini, sekali lagi, ketika kami menerapkan context_switch.
Pengantar singkat: Daftar. Daftarkan file. Daftarkan windows
Sekarang setelah kami sedikit memahami tumpukannya, kami perlu memahami bagaimana informasi disajikan di dalamnya. Untuk melakukan ini, kita perlu memperkenalkan beberapa konsep lagi.
File register (RF) adalah sekumpulan semua register. Ada dua file register yang kita butuhkan: satu file yang menghubungkan informasi (file berantai - CF), yang lain disebut file register (RF), ia menyimpan register "operasional", yang disimpan pada tumpukan prosedural.
Jendela register adalah area (kumpulan register) dari file register yang saat ini tersedia.
Saya akan jelaskan lebih detail. Apa set register, saya pikir, tidak ada yang perlu menjelaskan.
Diketahui bahwa salah satu hambatan dalam arsitektur x86 adalah sejumlah kecil register. Dalam arsitektur RISC dengan register lebih sederhana, biasanya sekitar 16 register, yang beberapa (2-3) ditempati untuk kebutuhan resmi. Mengapa tidak membuat 128 register saja, karena tampaknya ini akan meningkatkan kinerja sistem? Jawabannya cukup sederhana: instruksi prosesor memerlukan tempat untuk menyimpan alamat register, dan jika ada banyak, banyak bit juga diperlukan untuk ini. Oleh karena itu, mereka pergi ke segala macam trik, membuat register bayangan, mendaftar bank, jendela dan sebagainya. Dengan register bayangan, maksud saya prinsip organisasi register di ARM. Jika terjadi gangguan atau situasi lain, maka set register yang berbeda dengan nama (nomor) yang sama tersedia, sementara informasi yang disimpan dalam set aslinya tetap ada. Bank register, pada kenyataannya, sangat mirip dengan register bayangan, sama sekali tidak ada pergantian hardware dari set register, dan programmer memilih bank mana (set register) untuk dihubungi sekarang.
Daftar jendela dirancang untuk mengoptimalkan pekerjaan dengan tumpukan. Seperti yang mungkin Anda pahami, dalam arsitektur "normal" Anda memasukkan suatu prosedur, menyimpan register ke stack (atau prosedur pemanggilan menghemat, tergantung pada perjanjian) dan Anda dapat menggunakan register, karena informasi tersebut sudah tersimpan di stack. Tetapi akses memori lambat, dan karenanya harus dihindari. Saat memasuki prosedur, mari kita buat satu set register baru tersedia, data yang lama akan disimpan, yang berarti Anda tidak perlu membuangnya ke dalam memori. Selain itu, ketika Anda kembali ke prosedur panggilan, jendela register sebelumnya juga akan kembali, oleh karena itu, semua data pada register akan relevan. Ini adalah konsep jendela register.

Jelas bahwa Anda masih perlu menyimpan register di stack (dalam memori), tetapi ini dapat dilakukan ketika jendela register gratis telah berakhir.
Dan apa yang harus dilakukan dengan input dan output register (argumen ketika memasukkan fungsi dan hasil yang dikembalikan)? Biarkan jendela berisi bagian dari register yang terlihat dari jendela sebelumnya, lebih tepatnya, bagian dari register akan tersedia untuk kedua jendela. Kemudian, secara umum, saat memanggil fungsi, Anda tidak harus mengakses memori. Misalkan register kita terlihat seperti ini

Yaitu, r0 di jendela pertama akan menjadi register yang sama dengan r2 di nol, dan r1 dari jendela pertama di register yang sama dengan r3. Artinya, menulis dalam r2 sebelum memanggil prosedur (mengubah nomor jendela) kita mendapatkan nilai dalam r0 dalam prosedur yang disebut. Prinsip ini disebut mekanisme rotating windows.
Mari kita optimalkan sedikit lagi, karena pencipta Elbrus melakukan hal itu. Biarkan windows yang kita miliki tidak akan menjadi ukuran yang tetap, tetapi variabel, ukuran jendela dapat diatur pada saat masuk ke dalam prosedur. Kami akan melakukan hal yang sama dengan jumlah register yang diputar. Ini tentu saja akan membawa kita ke beberapa masalah, karena jika di jendela yang dapat diputar klasik, ada indeks jendela di mana ditentukan bahwa Anda perlu menyimpan data dari file register ke stack atau memuatnya. Tetapi jika Anda memasukkan bukan indeks jendela, tetapi indeks register dari mana jendela kita saat ini dimulai, maka masalah ini tidak akan muncul. Di Elbrus, indeks ini terdapat dalam register PSHTP (untuk tumpukan prosedur PS) dan PCSHTP (untuk tumpukan informasi prosedural PCS). Dokumentasi mengacu pada "penunjuk perangkat keras ke bagian atas tumpukan". Sekarang Anda dapat mencoba lagi untuk membaca tentang tumpukan, saya pikir itu akan menjadi lebih jelas.
Seperti yang Anda pahami, mekanisme seperti itu menyiratkan bahwa Anda memiliki kemampuan untuk mengendalikan apa yang ada dalam memori. Yaitu, menyinkronkan file register dan stack. Maksud saya seorang programmer sistem. Jika Anda seorang programmer aplikasi, peralatan akan memberikan entri transparan dan keluar dari prosedur. Yaitu, jika tidak ada cukup register ketika mencoba memilih jendela baru, maka jendela register akan secara otomatis “pump out”. Dalam hal ini, semua data dari file register akan disimpan pada tumpukan yang sesuai (dalam memori), dan "penunjuk ke bagian atas perangkat keras tumpukan" (indeks offset) akan diatur ulang ke nol. Demikian pula, menukar file register dari tumpukan terjadi secara otomatis. Tetapi jika Anda sedang mengembangkan, misalnya, pengalihan konteks, yang persis seperti yang kami lakukan, maka Anda memerlukan mekanisme untuk bekerja dengan bagian tersembunyi dari file register. Di Elbrus, perintah FLUSHR dan FLUSHC digunakan untuk ini. FLUSHR - membersihkan file register, semua jendela kecuali yang sekarang dibilas ke tumpukan prosedural, indeks PSHTP dengan demikian diatur ulang ke nol. FLUSHC - membersihkan file informasi yang mengikat, semuanya kecuali jendela saat ini dibuang ke tumpukan informasi yang mengikat, indeks PCSHTP juga diatur ulang ke nol.
Pengantar singkat: Implementasi di Elbrus
Sekarang kita telah membahas pekerjaan yang tidak jelas dengan register dan tumpukan, kita akan berbicara lebih spesifik tentang berbagai situasi di Elbrus.
Ketika kita memasuki fungsi berikutnya, prosesor membuat dua jendela: jendela di tumpukan PS dan jendela di tumpukan PCS.
Sebuah jendela di tumpukan PCS berisi informasi yang diperlukan untuk kembali dari suatu fungsi: misalnya, IP (Instruction Pointer) dari instruksi di mana Anda harus kembali dari fungsi tersebut. Dengan ini, semuanya kurang lebih jelas.
Jendela pada tumpukan PS sedikit lebih rumit. Konsep register dari jendela saat ini diperkenalkan. Di jendela ini Anda akan memiliki akses ke register jendela saat ini -% dr0,% dr1, ...,% dr15, ... Artinya, bagi kami, sebagai pengguna, mereka selalu diberi nomor dari 0, tetapi ini penomoran relatif terhadap alamat dasar dari jendela saat ini. Melalui register ini, argumen dilewatkan ketika fungsi dipanggil, dan nilai dikembalikan, dan fungsi digunakan sebagai register tujuan umum di dalam fungsi. Sebenarnya, ini dijelaskan ketika mempertimbangkan mekanisme rotating register windows.
Ukuran jendela register di Elbrus dapat dikontrol. Ini, seperti yang saya katakan, diperlukan untuk optimasi. Sebagai contoh, dalam suatu fungsi kita hanya perlu 4 register untuk melewati argumen dan beberapa perhitungan, dalam hal ini programmer (atau kompiler) memutuskan berapa banyak register yang dialokasikan untuk fungsi tersebut, dan berdasarkan ini menetapkan ukuran jendela. Ukuran jendela diatur oleh operasi setwd:
setwd wsz=0x10
Menentukan ukuran jendela dalam hal register quad (register 128-bit).

Sekarang, katakanlah Anda ingin memanggil suatu fungsi dari suatu fungsi. Untuk ini, konsep jendela register yang sudah diputar diterapkan. Gambar di atas menunjukkan sebuah fragmen dari file register di mana fungsi dengan jendela 1 (hijau) memanggil fungsi dengan jendela 2 (oranye). Di masing-masing dari dua fungsi ini Anda akan memiliki akses ke% dr0 Anda,% dr1, ... Tetapi argumen akan diteruskan melalui register putar yang disebut. Dengan kata lain, bagian dari register window 1 akan menjadi register window 2 (perhatikan bahwa kedua windows ini bersilangan). Register ini juga diatur oleh jendela (lihat register Rotary pada gambar) dan memiliki alamat% db [0],% db [1], ... Dengan demikian, register% dr0 di jendela 2 tidak lebih dari% db [0] yang terdaftar di jendela 1.
Jendela register rotasi diatur oleh operasi setbn:
setbn rbs = 0x3, rsz = 0x8
rbs menetapkan ukuran jendela yang diputar, dan rsz menetapkan alamat dasar, tetapi relatif terhadap jendela register saat ini. Yaitu Di sini kami telah mengalokasikan 3 register, mulai dari tanggal 8.
Berdasarkan hal tersebut di atas, kami menunjukkan seperti apa fungsi panggilan itu. Untuk mempermudah, kami menganggap bahwa fungsi tersebut mengambil satu argumen:
void my_func(uint64_t a) { }
Kemudian, untuk memanggil fungsi ini, Anda perlu menyiapkan jendela register putar (kami telah melakukan ini melalui setbn). Selanjutnya, dalam register% db0 kita menaruh nilai yang akan diteruskan ke my_func. Setelah ini, Anda perlu menghubungi instruksi PANGGILAN dan jangan lupa memberitahunya di mana jendela register yang diputar dimulai. Kami melewatkan persiapan untuk panggilan (perintah disp) sekarang, karena tidak peka huruf besar-kecil. Akibatnya, dalam assembler, panggilan ke fungsi ini akan terlihat seperti ini:
addd 0, %dr9, %db[0] disp %ctpr1, my_func call %ctpr1, wbs = 0x8
Jadi, dengan register sedikit tahu. Sekarang mari kita lihat tumpukan informasi yang mengikat. Ia menyimpan register CR yang disebut. Bahkan, dua - CR0, CR1. Dan mereka sudah berisi informasi yang diperlukan untuk kembali dari fungsi.

Register CR0 dan CR1 dari jendela fungsi yang disebut fungsi dengan register ditandai oranye berwarna hijau. Register CR0 berisi Pointer Instruksi dari fungsi panggilan dan file predikat tertentu (File Predikat PF), sebuah cerita tentang hal itu jelas di luar ruang lingkup artikel ini.
Register CR1 berisi data seperti PSR (status pengolah kata), nomor jendela, ukuran jendela, dan sebagainya. Di Elbrus, semuanya sangat fleksibel sehingga setiap prosedur menyimpan informasi dalam CR1 bahkan tentang apakah operasi floating point termasuk dalam prosedur, dan register yang berisi informasi tentang pengecualian perangkat lunak, tetapi untuk ini, tentu saja, Anda harus membayar untuk menyimpan informasi tambahan.
Sangat penting untuk tidak lupa bahwa file register dan file informasi yang mengikat dapat dipompa keluar dan ditukar keluar dari memori utama dan sebaliknya (dari tumpukan PS dan PCS yang dijelaskan di atas). Poin ini penting ketika mengimplementasikan setjmp yang diuraikan nanti.
SETJMP / LONGJMP
Dan akhirnya, setidaknya entah bagaimana memahami bagaimana tumpukan dan register diatur di Elbrus, Anda dapat mulai melakukan sesuatu yang bermanfaat, yaitu, menambahkan fungsionalitas baru ke Embox.
Di Embox, sistem pengujian unit memerlukan setjmp / longjmp, jadi kami harus mengimplementasikan fungsi-fungsi ini.
Untuk implementasi, diharuskan untuk menyimpan / mengembalikan register: CR0, CR1, PSP, PCSP, USD, - sudah akrab bagi kita dari pengantar singkat. Sebenarnya, tabungan / pemulihan diterapkan di dahi kita, tetapi ada nuansa signifikan yang sering diisyaratkan dalam deskripsi tumpukan dan register, yaitu: tumpukan harus disinkronkan, karena mereka tidak hanya terletak di memori, tetapi juga dalam file register. Nuansa ini berarti bahwa Anda harus mengurus beberapa fitur, yang tanpanya tidak ada yang berfungsi.
Fitur pertama adalah untuk menonaktifkan interupsi selama menyimpan dan mengembalikan. Ketika mengembalikan interupsi, adalah wajib untuk melarangnya, jika tidak suatu situasi dapat muncul di mana kita memasuki interrupt handler dengan tumpukan setengah beralih (mengacu pada memompa keluar file register yang ditukar sebagaimana dijelaskan dalam "deskripsi singkat"). Dan ketika menyimpan, masalahnya adalah setelah memasukkan dan keluar dari interupsi, prosesor dapat kembali menukar sebagian file register dari RAM (dan ini akan merusak kondisi invarian PSHTP = 0 dan PSCHTP = 0, sedikit lebih banyak tentang mereka). Itu sebabnya, baik di setjmp dan longjmp, interupsi harus dinonaktifkan. Perlu juga dicatat di sini bahwa spesialis dari MCST menyarankan kami untuk menggunakan tanda kurung atom daripada menonaktifkan interupsi, tetapi untuk saat ini kami menggunakan implementasi yang paling sederhana (dapat dipahami).
Fitur kedua terkait dengan memompa / memompa file register dari memori. Itu adalah sebagai berikut. File register memiliki ukuran terbatas dan oleh karena itu sering dipompa ke dalam memori dan sebaliknya. Oleh karena itu, jika kita hanya menyimpan nilai register PSP dan PSHTP, maka kita akan memperbaiki nilai pointer saat ini dalam memori dan dalam file register. Tetapi karena file register berubah, pada saat restorasi konteks itu akan menunjukkan data yang sudah salah (bukan yang kita "simpan"). Untuk menghindari hal ini, Anda perlu mem-flush seluruh file register ke dalam memori. Jadi, ketika menyimpan dalam setjmp, kami memiliki register PSP.ind di memori dan register PSHTP.ind di jendela register. Ternyata Anda harus menyimpan seluruh register PCSP.ind + PCSHTP.ind. Berikut ini adalah fungsi yang melakukan operasi ini:
.type update_pcsp_ind,@function $update_pcsp_ind: setwd wsz = 0x4, nfx = 0x0 shld %dr1, (64 - 10), %dr1 shrd %dr1, (64 - 10), %dr1 addd %dr1, %dr0, %dr0 E2K_ASM_RETURN
Penting juga untuk memperjelas poin kecil dalam kode ini yang dijelaskan dalam komentar, yaitu, perlu untuk secara program memperluas karakter dalam indeks PCSHTP.ind, karena indeks dapat negatif dan disimpan dalam kode tambahan. Untuk melakukan ini, pertama-tama kita beralih ke (64-10) ke kiri (register 64-bit), ke bidang 10-bit, dan kemudian kembali.
Hal yang sama berlaku untuk PSP (tumpukan prosedur)
.type update_psp_ind,@function $update_psp_ind: setwd wsz = 0x4, nfx = 0x0 shld %dr1, (64 - 12), %dr1 shrd %dr1, (64 - 12), %dr1 muld %dr1, 2, %dr1 addd %dr1, %dr0, %dr0 E2K_ASM_RETURN
Dengan sedikit perbedaan (bidang adalah 12 bit, dan register dihitung di sana dalam istilah 128-bit, yaitu, nilainya harus dikalikan 2).
Setjmp kode itu sendiri
C_ENTRY(setjmp): setwd wsz = 0x14, nfx = 0x0 setbn rsz = 0x3, rbs = 0x10, rcur = 0x0 disp %ctpr1, ipl_save ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr9 rrd %cr0.hi, %dr1 rrd %cr1.lo, %dr2 rrd %cr1.hi, %dr3 rrd %usd.lo, %dr4 rrd %usd.hi, %dr5 rrd %psp.hi, %dr6 rrd %pshtp, %dr7 addd 0, %dr6, %db[0] addd 0, %dr7, %db[1] disp %ctpr1, update_psp_ind ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr6 rrd %pcsp.hi, %dr7 rrd %pcshtp, %dr8 addd 0, %dr7, %db[0] addd 0, %dr8, %db[1] disp %ctpr1, update_pcsp_ind ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr7 std %dr1, [%dr0 + E2K_JMBBUFF_CR0_HI] std %dr2, [%dr0 + E2K_JMBBUFF_CR1_LO] std %dr3, [%dr0 + E2K_JMBBUFF_CR1_HI] std %dr4, [%dr0 + E2K_JMBBUFF_USD_LO] std %dr5, [%dr0 + E2K_JMBBUFF_USD_HI] std %dr6, [%dr0 + E2K_JMBBUFF_PSP_HI] std %dr7, [%dr0 + E2K_JMBBUFF_PCSP_HI] addd 0, %dr9, %db[0] disp %ctpr1, ipl_restore ipd 3 call %ctpr1, wbs = 0x10 adds 0, 0, %r0 E2K_ASM_RETURN
Ketika menerapkan longjmp, penting untuk tidak melupakan sinkronisasi kedua file register, oleh karena itu, Anda perlu membilas tidak hanya jendela register (flushr), tetapi juga flush file binder (flushc). Mari kita gambarkan makro:
#define E2K_ASM_FLUSH_CPU \ flushr; \ nop 2; \ flushc; \ nop 3;
Sekarang semua informasi ada dalam memori, kita dapat dengan aman melakukan pemulihan pemulihan dalam longjmp.
C_ENTRY(longjmp): setwd wsz = 0x14, nfx = 0x0 setbn rsz = 0x3, rbs = 0x10, rcur = 0x0 disp %ctpr1, ipl_save ipd 3 call %ctpr1, wbs = 0x10 addd 0, %db[0], %dr9 E2K_ASM_FLUSH_CPU ldd [%dr0 + E2K_JMBBUFF_CR0_HI], %dr2 ldd [%dr0 + E2K_JMBBUFF_CR1_LO], %dr3 ldd [%dr0 + E2K_JMBBUFF_CR1_HI], %dr4 ldd [%dr0 + E2K_JMBBUFF_USD_LO], %dr5 ldd [%dr0 + E2K_JMBBUFF_USD_HI], %dr6 ldd [%dr0 + E2K_JMBBUFF_PSP_HI], %dr7 ldd [%dr0 + E2K_JMBBUFF_PCSP_HI], %dr8 rwd %dr2, %cr0.hi rwd %dr3, %cr1.lo rwd %dr4, %cr1.hi rwd %dr5, %usd.lo rwd %dr6, %usd.hi rwd %dr7, %psp.hi rwd %dr8, %pcsp.hi addd 0, %dr9, %db[0] disp %ctpr1, ipl_restore ipd 3 call %ctpr1, wbs = 0x10 adds 0, %r1, %r0 E2K_ASM_RETURN
Sakelar konteks
Setelah kami menemukan setjmp / longjmp, implementasi dasar context_switch tampak cukup jelas bagi kami. Memang, seperti dalam kasus pertama, kita perlu menyimpan / mengembalikan register yang menghubungkan informasi dan tumpukan, ditambah kita perlu mengembalikan register status prosesor (UPSR) dengan benar.
Saya akan jelaskan. Seperti dalam kasus setjmp, saat menyimpan register, Anda harus terlebih dahulu mengatur ulang file register dan file informasi yang mengikat ke memori (flushr + flushc). Setelah itu, kita perlu menyimpan nilai register CR0 dan CR1 saat ini sehingga ketika kita kembali, lompat persis ke tempat aliran saat ini dialihkan. Selanjutnya, kita menyimpan deskriptor PS, PCS, dan tumpukan AS. Dan akhirnya, Anda perlu menjaga pemulihan yang benar dari mode interupsi - untuk tujuan ini, kami juga menyimpan register UPSR.
Kode assembler context_switch:
C_ENTRY(context_switch): setwd wsz = 0x10, nfx = 0x0 rrd %upsr, %dr2 std %dr2, [%dr0 + E2K_CTX_UPSR] rrd %upsr, %dr2 andnd %dr2, (UPSR_IE | UPSR_NMIE), %dr2 rwd %dr2, %upsr E2K_ASM_FLUSH_CPU rrd %cr0.lo, %dr2 rrd %cr0.hi, %dr3 rrd %cr1.lo, %dr4 rrd %cr1.hi, %dr5 std %dr2, [%dr0 + E2K_CTX_CR0_LO] std %dr3, [%dr0 + E2K_CTX_CR0_HI] std %dr4, [%dr0 + E2K_CTX_CR1_LO] std %dr5, [%dr0 + E2K_CTX_CR1_HI] rrd %usd.lo, %dr3 rrd %usd.hi, %dr4 rrd %psp.lo, %dr5 rrd %psp.hi, %dr6 rrd %pcsp.lo, %dr7 rrd %pcsp.hi, %dr8 std %dr3, [%dr0 + E2K_CTX_USD_LO] std %dr4, [%dr0 + E2K_CTX_USD_HI] std %dr5, [%dr0 + E2K_CTX_PSP_LO] std %dr6, [%dr0 + E2K_CTX_PSP_HI] std %dr7, [%dr0 + E2K_CTX_PCSP_LO] std %dr8, [%dr0 + E2K_CTX_PCSP_HI] ldd [%dr1 + E2K_CTX_CR0_LO], %dr2 ldd [%dr1 + E2K_CTX_CR0_HI], %dr3 ldd [%dr1 + E2K_CTX_CR1_LO], %dr4 ldd [%dr1 + E2K_CTX_CR1_HI], %dr5 rwd %dr2, %cr0.lo rwd %dr3, %cr0.hi rwd %dr4, %cr1.lo rwd %dr5, %cr1.hi ldd [%dr1 + E2K_CTX_USD_LO], %dr3 ldd [%dr1 + E2K_CTX_USD_HI], %dr4 ldd [%dr1 + E2K_CTX_PSP_LO], %dr5 ldd [%dr1 + E2K_CTX_PSP_HI], %dr6 ldd [%dr1 + E2K_CTX_PCSP_LO], %dr7 ldd [%dr1 + E2K_CTX_PCSP_HI], %dr8 rwd %dr3, %usd.lo rwd %dr4, %usd.hi rwd %dr5, %psp.lo rwd %dr6, %psp.hi rwd %dr7, %pcsp.lo rwd %dr8, %pcsp.hi ldd [%dr1 + E2K_CTX_UPSR], %dr2 rwd %dr2, %upsr E2K_ASM_RETURN
Poin penting lainnya adalah inisialisasi thread OS. Di Embox, setiap utas memiliki prosedur utama tertentu
void _NORETURN thread_trampoline(void);
di mana semua pekerjaan lebih lanjut dari aliran akan dieksekusi. Jadi, kita perlu entah bagaimana mempersiapkan tumpukan untuk memanggil fungsi ini, di sinilah kita dihadapkan dengan fakta bahwa ada tiga tumpukan, dan mereka tidak tumbuh ke arah yang sama. Dengan arsitektur, kami membuat aliran dengan tumpukan tunggal, atau lebih tepatnya, ia memiliki satu tempat di bawah tumpukan, di bagian atas kami memiliki struktur yang menggambarkan aliran itu sendiri, dan seterusnya, di sini kami harus mengurus tumpukan yang berbeda, jangan lupa bahwa mereka harus disejajarkan dengan 4 kB, jangan lupa segala macam hak akses dan sebagainya.
Akibatnya, saat ini kami memutuskan bahwa kami akan membagi ruang di bawah tumpukan menjadi tiga bagian, seperempat di bawah tumpukan informasi yang mengikat, seperempat di bawah tumpukan prosedural, dan setengah di bawah tumpukan pengguna.
Saya membawa kode sehingga Anda dapat mengevaluasi seberapa besar itu, Anda perlu mempertimbangkan bahwa ini adalah inisialisasi minimal. #define E2K_STACK_ALIGN (1UL << 12) #define round_down(x, bound) ((x) & ~((bound) - 1)) /* Reserve 1/4 for PSP stack, 1/4 for PCSP stack, and 1/2 for USD stack */ #define PSP_CALC_STACK_BASE(sp, size) binalign_bound(sp - size, E2K_STACK_ALIGN) #define PSP_CALC_STACK_SIZE(sp, size) binalign_bound((size) / 4, E2K_STACK_ALIGN) #define PCSP_CALC_STACK_BASE(sp, size) \ (PSP_CALC_STACK_BASE(sp, size) + PSP_CALC_STACK_SIZE(sp, size)) #define PCSP_CALC_STACK_SIZE(sp, size) binalign_bound((size) / 4, E2K_STACK_ALIGN) #define USD_CALC_STACK_BASE(sp, size) round_down(sp, E2K_STACK_ALIGN) #define USD_CALC_STACK_SIZE(sp, size) \ round_down(USD_CALC_STACK_BASE(sp, size) - PCSP_CALC_STACK_BASE(sp, size),\ E2K_STACK_ALIGN) static void e2k_calculate_stacks(struct context *ctx, uint64_t sp, uint64_t size) { uint64_t psp_size, pcsp_size, usd_size; log_debug("Stacks:\n"); ctx->psp_lo |= PSP_CALC_STACK_BASE(sp, size) << PSP_BASE; ctx->psp_lo |= E2_RWAR_RW_ENABLE << PSP_RW; psp_size = PSP_CALC_STACK_SIZE(sp, size); assert(psp_size); ctx->psp_hi |= psp_size << PSP_SIZE; log_debug(" PSP.base=0x%lx, PSP.size=0x%lx\n", PSP_CALC_STACK_BASE(sp, size), psp_size); ctx->pcsp_lo |= PCSP_CALC_STACK_BASE(sp, size) << PCSP_BASE; ctx->pcsp_lo |= E2_RWAR_RW_ENABLE << PCSP_RW; pcsp_size = PCSP_CALC_STACK_SIZE(sp, size); assert(pcsp_size); ctx->pcsp_hi |= pcsp_size << PCSP_SIZE; log_debug(" PCSP.base=0x%lx, PCSP.size=0x%lx\n", PCSP_CALC_STACK_BASE(sp, size), pcsp_size); ctx->usd_lo |= USD_CALC_STACK_BASE(sp, size) << USD_BASE; usd_size = USD_CALC_STACK_SIZE(sp, size); assert(usd_size); ctx->usd_hi |= usd_size << USD_SIZE; log_debug(" USD.base=0x%lx, USD.size=0x%lx\n", USD_CALC_STACK_BASE(sp, size), usd_size); } static void e2k_calculate_crs(struct context *ctx, uint64_t routine_addr) { uint64_t usd_size = (ctx->usd_hi >> USD_SIZE) & USD_SIZE_MASK; /* Reserve space in hardware stacks for @routine_addr */ /* Remark: We do not update psp.hi to reserve space for arguments, * since routine do not accepts any arguments. */ ctx->pcsp_hi |= SZ_OF_CR0_CR1 << PCSP_IND; ctx->cr0_hi |= (routine_addr >> CR0_IP) << CR0_IP; ctx->cr1_lo |= PSR_ALL_IRQ_ENABLED << CR1_PSR; /* Divide on 16 because it field contains size in terms * of 128 bit values. */ ctx->cr1_hi |= (usd_size >> 4) << CR1_USSZ; } void context_init(struct context *ctx, unsigned int flags, void (*routine_fn)(void), void *sp, unsigned int stack_size) { memset(ctx, 0, sizeof(*ctx)); e2k_calculate_stacks(ctx, sp, stack_size); e2k_calculate_crs(ctx, (uint64_t) routine_fn); if (!(flags & CONTEXT_IRQDISABLE)) { ctx->upsr |= (UPSR_IE | UPSR_NMIE); } }
Artikel itu juga berisi pekerjaan dengan interupsi, pengecualian, dan timer, tetapi karena ternyata sangat besar, kami memutuskan untuk membicarakannya di bagian
selanjutnya .
Untuk berjaga-jaga, saya ulangi, bahan ini bukan dokumentasi resmi! Untuk dukungan resmi, dokumentasi, dan yang lainnya, Anda perlu menghubungi ICST secara langsung. Kode di
Embox , tentu saja, terbuka, tetapi untuk mengkompilasinya, Anda memerlukan cross-compiler, yang, sekali lagi, dapat diperoleh dari
MCST .