RISC-V dari awal

Dalam artikel ini, kami mengeksplorasi berbagai konsep tingkat rendah (kompilasi dan tata letak, runtime primitif, assembler, dan banyak lagi) melalui prisma arsitektur RISC-V dan ekosistemnya. Saya sendiri seorang pengembang web, saya tidak melakukan apa pun di tempat kerja, tapi ini sangat menarik bagi saya, dari sinilah artikel itu berasal! Bergabunglah dengan saya dalam perjalanan yang sibuk ini ke kedalaman kekacauan tingkat rendah.

Pertama, mari kita bicara sedikit tentang RISC-V dan pentingnya arsitektur ini, mengatur toolchain RISC-V dan menjalankan program C sederhana pada perangkat keras RISC-V yang ditiru.

Isi


  1. Apa itu RISC-V?
  2. Mengkonfigurasi Alat QEMU dan RISC-V
  3. Hai RISC-V!
  4. Pendekatan naif
  5. Mengangkat tirai -v
  6. Cari tumpukan kami
  7. Tata letak
  8. Hentikan itu! Hammertime! Runtime!
  9. Debug tapi sekarang nyata
  10. Apa selanjutnya
  11. Opsional

Apa itu RISC-V?


RISC-V adalah arsitektur set instruksi gratis. Proyek ini berasal dari University of California di Berkeley pada 2010. Peran penting dalam keberhasilannya dimainkan oleh keterbukaan kode dan kebebasan penggunaan, yang sangat berbeda dari banyak arsitektur lainnya. Ambil ARM: untuk membuat prosesor yang kompatibel, Anda harus membayar uang muka $ 1 juta hingga $ 10 juta, dan juga membayar royalti 0,5βˆ’2% dari penjualan . Model yang bebas dan terbuka menjadikan RISC-V pilihan yang menarik bagi banyak orang, termasuk untuk pemula yang tidak dapat membayar lisensi untuk ARM atau prosesor lain, untuk peneliti akademis dan (jelas) untuk komunitas sumber terbuka.

Pertumbuhan cepat dalam popularitas RISC-V tidak luput dari perhatian. ARM meluncurkan situs yang mencoba (agak tidak berhasil) untuk menyoroti dugaan manfaat ARM dibandingkan RISC-V (situs tersebut sudah ditutup). Proyek RISC-V didukung oleh banyak perusahaan besar , termasuk Google, Nvidia dan Western Digital.

Mengkonfigurasi Alat QEMU dan RISC-V


Kami tidak dapat menjalankan kode pada prosesor RISC-V sampai kami mengatur lingkungan. Untungnya, ini tidak memerlukan prosesor fisik RISC-V, sebagai gantinya, kami mengambil qemu . Ikuti instruksi untuk menginstal sistem operasi Anda . Saya memiliki MacOS, jadi cukup masukkan satu perintah:

# also available via MacPorts - `sudo port install qemu` brew install qemu 

Dengan mudah, qemu hadir dengan beberapa mesin siap-pakai (lihat opsi qemu-system-riscv32 -machine ).

Selanjutnya, instal OpenOCD untuk alat RISC-V dan RISC-V.

Unduh perangkat RISC-V OpenOCD dan RISC-V yang sudah jadi di sini .
Kami mengekstrak file ke direktori mana pun, saya memilikinya ~/usys/riscv . Ingat untuk digunakan di masa depan.

 mkdir -p ~/usys/riscv cd ~/Downloads cp openocd-<date>-<platform>.tar.gz ~/usys/riscv cp riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz ~/usys/riscv cd ~/usys/riscv tar -xvf openocd-<date>-<platform>.tar.gz tar -xvf riscv64-unknown-elf-gcc-<date>-<platform>.tar.gz 

Tetapkan variabel lingkungan RISCV_OPENOCD_PATH dan RISCV_PATH sehingga program lain dapat menemukan rantai alat kami. Ini mungkin terlihat berbeda tergantung pada OS dan shell: Saya menambahkan path ke file ~/.zshenv .

 # I put these two exports directly in my ~/.zshenv file - you may have to do something else. export RISCV_OPENOCD_PATH="$HOME/usys/riscv/openocd-<date>-<version>" export RISCV_PATH="$HOME/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>" # Reload .zshenv with our new environment variables. Restarting your shell will have a similar effect. source ~/.zshenv 

Buat tautan simbolis di /usr/local/bin untuk file yang dapat dieksekusi ini sehingga Anda dapat menjalankannya kapan saja tanpa menentukan path lengkap ke ~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/riscv64-unknown-elf-gcc .

 # Symbolically link our gcc executable into /usr/local/bin. Repeat this process for any other executables you want to quickly access. ln -s ~/usys/riscv/riscv64-unknown-elf-gcc-8.2.0-<date>-<version>/bin/riscv64-unknown-elf-gcc /usr/local/bin 

Dan voila, kami memiliki toolkit RISC-V yang berfungsi! Semua executable kami, seperti riscv64-unknown-elf-gcc , riscv64-unknown-elf-gdb , riscv64-unknown-elf-ld dan lainnya, ada di ~/usys/riscv/riscv64-unknown-elf-gcc-<date>-<version>/bin/ .

Hai RISC-V!


26 Mei 2019 Tambalan:

Sayangnya, karena bug di RISC-V QEMU, program 'hello world' kebebasan-e-sdk di QEMU tidak lagi berfungsi. Sebuah tambalan telah dirilis untuk mengatasi masalah ini, tetapi untuk saat ini, lewati bagian ini. Program ini tidak akan diperlukan di bagian artikel selanjutnya. Saya melacak situasi dan memperbarui artikel setelah memperbaiki bug.

Lihat komentar ini untuk informasi lebih lanjut.

Dengan alat yang disiapkan, mari kita jalankan program RISC-V sederhana. Mari kita mulai dengan mengkloning repositori SiFive freedom-e-sdk :

 cd ~/wherever/you/want/to/clone/this git clone --recursive https://github.com/sifive/freedom-e-sdk.git cd freedom-e-sdk 

Secara tradisi , mari kita mulai dengan program 'Hello, world' dari repositori freedom-e-sdk . Kami menggunakan Makefile siap pakai yang mereka sediakan untuk mengkompilasi program ini dalam mode debug:

 make PROGRAM=hello TARGET=sifive-hifive1 CONFIGURATION=debug software 

Dan jalankan di QEMU:

 qemu-system-riscv32 -nographic -machine sifive_e -kernel software/hello/debug/hello.elf Hello, World! 

Ini awal yang bagus. Anda dapat menjalankan contoh lain dari freedom-e-sdk . Setelah itu, kita akan menulis dan mencoba men-debug program kita sendiri di C.

Pendekatan naif


Mari kita mulai dengan program sederhana yang menambahkan dua angka tanpa batas.

 cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; } 

Kami ingin menjalankan program ini, dan hal pertama yang kami perlukan untuk mengkompilasinya untuk prosesor RISC-V.

 # -O0 to disable all optimizations. Without this, GCC might optimize # away our infinite addition since the result 'c' is never used. # -g to tell GCC to preserve debug info in our executable. riscv64-unknown-elf-gcc add.c -O0 -g 

Ini menciptakan file a.out , yang gcc default ke file yang dapat dieksekusi. Sekarang jalankan file ini di qemu :

 # -machine tells QEMU which among our list of available machines we want to # run our executable against. Run qemu-system-riscv64 -machine help to list # all available machines. # -m is the amount of memory to allocate to our virtual machine. # -gdb tcp::1234 tells QEMU to also start a GDB server on localhost:1234 where # TCP is the means of communication. # -kernel tells QEMU what we're looking to run, even if our executable isn't # exactly a "kernel". qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out 

Kami memilih mesin virt yang riscv-qemu awalnya datang riscv-qemu .

Sekarang program kami berjalan di dalam QEMU dengan server GDB di localhost:1234 , kami terhubung dengan klien RISC-V GDB dari terminal terpisah:

 # --tui gives us a (t)extual (ui) for our GDB session. # While we can start GDB without any arguments, specifying 'a.out' tells GDB # to load debug symbols from that file for the newly created session. riscv64-unknown-elf-gdb --tui a.out 

Dan kita berada di dalam GDB!

  GDB ini dikonfigurasi sebagai "--host = x86_64-apple-darwin17.7.0 --target = riscv64-unknown-elf".  β”‚
 Ketik "tampilkan konfigurasi" untuk detail konfigurasi.  β”‚
 Untuk instruksi pelaporan bug, silakan lihat: β”‚
 <http://www.gnu.org/software/gdb/bugs/>.  β”‚
 Temukan manual GDB dan sumber dokumentasi lainnya secara online di: β”‚
     <http://www.gnu.org/software/gdb/documentation/>.  β”‚
                                                                                                       β”‚
 Untuk bantuan, ketik "bantuan".  β”‚
 Ketik "kata yang tepat" untuk mencari perintah yang terkait dengan "kata" ... β”‚
 Membaca simbol dari a.out ... β”‚
 (gdb) 

Kita dapat mencoba menjalankan run atau start perintah untuk file yang dapat dieksekusi a.out di GDB, tetapi saat ini ini tidak akan berfungsi karena alasan yang jelas. Kami mengkompilasi program sebagai riscv64-unknown-elf-gcc , sehingga host harus dijalankan pada arsitektur riscv64 .

Tapi ada jalan keluar! Situasi ini adalah salah satu alasan utama untuk keberadaan model klien-server GDB. Kita dapat mengambil file executable riscv64-unknown-elf-gdb dan alih-alih meluncurkannya pada host tentukan beberapa target jarak jauh (server GDB). Seperti yang Anda ingat, kami baru saja memulai riscv-qemu dan memberitahu kami untuk memulai server GDB di localhost:1234 . Cukup sambungkan ke server ini:

  (gdb) target jarak jauh: 1234 β”‚
 Remote debugging menggunakan: 1234 

Sekarang Anda dapat mengatur beberapa breakpoints:

 (gdb) b main Breakpoint 1 at 0x1018e: file add.c, line 2. (gdb) b 5 # this is the line within the forever-while loop. int c = a + b; Breakpoint 2 at 0x1019a: file add.c, line 5. 

Dan akhirnya, tentukan GDB continue (disingkat perintah c ) sampai kita mencapai breakpoint:

 (gdb) c Continuing. 

Anda akan segera menyadari bahwa prosesnya tidak berakhir dengan cara apa pun. Ini aneh ... bukankah kita harus segera mencapai breakpoint b 5 ? Apa yang terjadi



Di sini Anda dapat melihat beberapa masalah:

  1. UI teks tidak dapat menemukan sumbernya. Antarmuka harus menampilkan kode kami dan breakpoints terdekat.
  2. GDB tidak melihat garis eksekusi saat ini ( L?? ) dan menampilkan penghitung 0x0 ( PC: 0x0 ).
  3. Beberapa teks di baris input, yang secara keseluruhan terlihat seperti ini: 0x0000000000000000 in ?? () 0x0000000000000000 in ?? ()

Dikombinasikan dengan fakta bahwa kami tidak dapat mencapai breakpoint, indikator-indikator ini menunjukkan: kami melakukan sesuatu yang salah. Tapi apa?

Mengangkat tirai -v


Untuk memahami apa yang terjadi, Anda perlu mengambil langkah mundur dan berbicara tentang bagaimana sebenarnya program C kami di bawah tenda bekerja. Fungsi main melakukan penambahan sederhana, tetapi apakah itu sebenarnya? Mengapa harus disebut main , bukan origin atau begin ? Menurut konvensi, semua file yang dapat dieksekusi mulai dieksekusi dengan fungsi main , tetapi sihir apa yang menyediakan perilaku ini?

Untuk menjawab pertanyaan-pertanyaan ini, mari kita ulangi tim GCC kami dengan flag -v untuk mendapatkan hasil yang lebih rinci tentang apa yang sebenarnya terjadi.

 riscv64-unknown-elf-gcc add.c -O0 -g -v 

Outputnya besar, jadi kami tidak akan melihat seluruh daftar. Penting untuk dicatat bahwa meskipun GCC secara resmi adalah kompiler, GCC juga merupakan default untuk kompilasi (untuk membatasi diri pada kompilasi dan perakitan, Anda harus menentukan flag -c ). Mengapa ini penting? Nah, lihat cuplikan dari output terperinci gcc :

  # Perintah `gcc -v` aktual menghasilkan path lengkap, tetapi cukup
 # panjang, jadi anggaplah variabel-variabel ini ada.
 # $ RV_GCC_BIN_PATH = / Pengguna / twilcock / usys / riscv / riscv64-unknown-elf-gcc- <tanggal> - <version> / bin /
 # $ RV_GCC_LIB_PATH = $ RV_GCC_BIN_PATH /../ lib / gcc / riscv64-unknown-elf / 8.2.0

 $ RV_GCC_BIN_PATH /../ libexec / gcc / riscv64-unknown-elf / 8.2.0 / collect2 \
   ... terpotong ... 
   $ RV_GCC_LIB_PATH /../../../../ riscv64-unknown-elf / lib / rv64imafdc / lp64d / crt0.o \ 
   $ RV_GCC_LIB_PATH / riscv64-unknown-elf / 8.2.0 / rv64imafdc / lp64d / crtbegin.o \
   -lgcc - mulai-grup -lc -lgloss --end-grup -lgcc \ 
   $ RV_GCC_LIB_PATH / rv64imafdc / lp64d / crtend.o
   ... terpotong ...
 COLLECT_GCC_OPTIONS = '- O0' '-g' '-v' '-march = rv64imafdc' '-mabi = lp64d' 

Saya mengerti bahwa meskipun dalam bentuk singkat ini banyak, jadi izinkan saya menjelaskannya. Pada baris pertama, gcc menjalankan program collect2 , meneruskan argumen crt0.o , crtbegin.o dan crtend.o , -lgcc dan --start-group flag. Deskripsi collect2 dapat ditemukan di sini : singkatnya, collect2 mengatur berbagai fungsi inisialisasi saat startup, membuat tata letak dalam satu atau lebih lintasan.

Dengan demikian, GCC mengkompilasi beberapa file crt dengan kode kami. Seperti yang bisa Anda tebak, crt berarti 'C runtime'. Di sini dijelaskan secara rinci apa crt dimaksudkan untuk setiap crt , tetapi kami tertarik pada crt0 , yang melakukan satu hal penting:

"Objek [crt0] ini diharapkan mengandung karakter _start , yang menunjukkan bootstrap program."

Inti dari "bootstrap" tergantung pada platform, tetapi biasanya melibatkan tugas-tugas penting seperti mengatur bingkai stack, meneruskan argumen baris perintah, dan memanggil main . Ya, kami akhirnya menemukan jawaban untuk pertanyaan: _start memanggil fungsi utama kami!

Cari tumpukan kami


Kami memecahkan satu teka-teki, tetapi bagaimana hal ini membawa kami lebih dekat ke tujuan semula - untuk menjalankan program C sederhana di gdb ? Masih untuk menyelesaikan beberapa masalah: yang pertama terkait dengan bagaimana crt0 mengkonfigurasi stack kita.

Seperti yang kita lihat di atas, gcc default untuk crt0 . Parameter default dipilih berdasarkan beberapa faktor:

  • Triplet target sesuai dengan struktur machine-vendor-operatingsystem . Kami memilikinya riscv64-unknown-elf
  • Arsitektur Target, rv64imafdc
  • Target ABI, lp64d

Biasanya semuanya berfungsi dengan baik, tetapi tidak untuk setiap prosesor RISC-V. Seperti disebutkan sebelumnya, salah satu tugas crt0 adalah mengkonfigurasi stack. Tapi dia tidak tahu di mana tepatnya tumpukan seharusnya untuk CPU kita ( -machine )? Dia tidak bisa melakukannya tanpa bantuan kita.

Dalam qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -kernel a.out kami menggunakan mesin virt . Untungnya, qemu memudahkan untuk membuang informasi mesin ke dump dtb (device tree blob).

 # Go to the ~/usys/riscv folder we created before and create a new dir # for our machine information. cd ~/usys/riscv && mkdir machines cd machines # Use qemu to dump info about the 'virt' machine in dtb (device tree blob) # format. # The data in this file represents hardware components of a given # machine / device / board. qemu-system-riscv64 -machine virt -machine dumpdtb=riscv64-virt.dtb 

Data dtb sulit dibaca karena pada dasarnya format biner, tetapi ada utilitas baris perintah dtc (kompilator hierarki perangkat) yang dapat mengonversi file menjadi sesuatu yang lebih mudah dibaca.

 # I'm running MacOS, so I use Homebrew to install this. If you're # running another OS you may need to do something else. brew install dtc # Convert our .dtb into a human-readable .dts (device tree source) file. dtc -I dtb -O dts -o riscv64-virt.dts riscv64-virt.dtb 

File outputnya adalah riscv64-virt.dts , di mana kita melihat banyak informasi menarik tentang virt : jumlah core prosesor yang tersedia, lokasi memori berbagai perangkat periferal, seperti UART, lokasi memori internal (RAM). Tumpukan harus ada di memori ini, jadi cari dengan grep :

 grep memory riscv64-virt.dts -A 3 memory@80000000 { device_type = "memory"; reg = <0x00 0x80000000 0x00 0x8000000>; }; 

Seperti yang Anda lihat, simpul ini memiliki 'memori' yang ditentukan sebagai device_type . Rupanya, kami menemukan apa yang kami cari. Dengan nilai-nilai di dalam reg = <...> ; Anda dapat menentukan di mana bank memori dimulai dan berapa panjangnya.

Dalam spesifikasi devicetree, kita melihat bahwa sintaks reg adalah jumlah pasangan (base_address, length) . Namun, ada empat arti di dalam reg . Aneh, bukankah dua nilai cukup untuk satu bank memori?

Sekali lagi, dari spesifikasi devicetree (mencari properti reg ) kami menemukan bahwa jumlah sel <u32> untuk menentukan alamat dan panjangnya ditentukan oleh properti #address-cells dan #size-cells dalam node induk (atau dalam node itu sendiri). Nilai-nilai ini tidak ditentukan dalam simpul memori kami, dan simpul memori induk hanyalah akar dari file. Mari kita lihat nilai-nilai ini:

 head -n8 riscv64-virt.dts /dts-v1/; / { #address-cells = <0x02>; #size-cells = <0x02>; compatible = "riscv-virtio"; model = "riscv-virtio,qemu"; 

Ternyata alamat dan panjangnya membutuhkan dua nilai 32-bit. Ini berarti bahwa dengan reg = <0x00 0x80000000 0x00 0x8000000>; memori kita dimulai 0x00 + 0x80000000 (0x80000000) dan menempati 0x00 + 0x8000000 (0x8000000) byte, yaitu berakhir pada 0x88000000 , yang sesuai dengan 128 megabyte.

Tata letak


Menggunakan qemu dan dtc kami menemukan alamat RAM di mesin virtual virt. Kita juga tahu bahwa gcc menyusun crt0 secara default, tanpa mengkonfigurasi stack seperti yang kita butuhkan. Tetapi bagaimana cara menggunakan informasi ini untuk akhirnya menjalankan dan men-debug program?

Karena crt0 tidak sesuai dengan kita, ada satu opsi yang jelas: tulis kode Anda sendiri, lalu susun dengan file objek yang kami peroleh setelah mengkompilasi program sederhana kami. crt0 kami perlu tahu di mana bagian atas tumpukan dimulai untuk menginisialisasi dengan benar. Kami dapat crt0 nilai 0x80000000 langsung ke crt0 , tetapi ini bukan solusi yang sangat cocok, dengan mempertimbangkan perubahan akun yang mungkin diperlukan di masa mendatang. Bagaimana jika kita ingin menggunakan CPU lain, seperti sifive_e , dengan karakteristik berbeda di emulator?

Untungnya, kita bukan orang pertama yang mengajukan pertanyaan ini, dan solusi yang baik sudah ada. GNU ld linker memungkinkan Anda untuk menentukan karakter yang tersedia dari crt0 kami. Kita dapat mendefinisikan simbol __stack_top cocok untuk prosesor yang berbeda.

Alih-alih menulis file tautan Anda sendiri dari awal, masuk akal untuk mengambil skrip default dengan ld dan memodifikasinya sedikit untuk mendukung karakter tambahan. Apa itu skrip tautan? Berikut ini deskripsi yang bagus :

Tujuan utama skrip linker adalah untuk menggambarkan bagaimana bagian file dicocokkan dalam input dan output, dan untuk mengontrol tata letak memori dari file output.

Mengetahui hal ini, mari salin skrip riscv64-unknown-elf-ld default riscv64-unknown-elf-ld ke file baru:

 cd ~/usys/riscv # Make a new dir for custom linker scripts out RISC-V CPUs may require. mkdir ld && cd ld # Copy the default linker script into riscv64-virt.ld riscv64-unknown-elf-ld --verbose > riscv64-virt.ld 

File ini memiliki banyak informasi menarik, lebih dari yang bisa kita bahas dalam artikel ini. Output terperinci dengan --Verbose mencakup informasi tentang versi ld , arsitektur yang didukung, dan banyak lagi. Ini semua baik untuk diketahui, tetapi sintaks seperti itu tidak dapat diterima dalam skrip tautan, jadi buka editor teks dan hapus semua yang tidak perlu dari file.

  vim riscv64-virt.ld

 # Hapus semua yang di atas dan termasuk baris =============
 GNU ld (GNU Binutils) 2.32
   Emulasi yang didukung:
    elf64lriscv
    elf32lriscv
 menggunakan skrip tautan internal:
 ===================================================
 / * Script untuk -z combreloc: menggabungkan dan mengurutkan bagian reloc * /
 / * Hak Cipta (C) 2014-2019 Free Software Foundation, Inc.
    Menyalin dan mendistribusikan skrip ini, dengan atau tanpa modifikasi,
    diizinkan dalam media apa pun tanpa royalti dengan ketentuan hak cipta
    pemberitahuan dan pemberitahuan ini dilestarikan.  * /
 OUTPUT_FORMAT ("elf64-littleriscv", "elf64-littleriscv",
	       "elf64-littleriscv")
 ... sisa skrip tautan ... 

Setelah itu, jalankan perintah MEMORY untuk secara manual menentukan di mana __stack_top akan berada. Temukan baris yang dimulai dengan OUTPUT_ARCH(riscv) , harus berada di bagian atas file, dan tambahkan perintah MEMORY di bawahnya:

 OUTPUT_ARCH(riscv) /* >>> Our addition. <<< */ MEMORY { /* qemu-system-risc64 virt machine */ RAM (rwx) : ORIGIN = 0x80000000, LENGTH = 128M } /* >>> End of our addition. <<< */ ENTRY(_start) 

Kami membuat blok memori yang disebut RAM , yang memungkinkan pembacaan ( r ), penulisan ( w ), dan penyimpanan kode yang dapat dieksekusi ( x ) diizinkan.

Hebat, kami telah mendefinisikan tata letak memori yang sesuai dengan spesifikasi mesin RISC-V kami yang hebat. Sekarang kamu bisa menggunakannya. Kami ingin meletakkan tumpukan kami di memori.

Anda perlu mendefinisikan karakter __stack_top . Buka skrip riscv64-virt.ld ( riscv64-virt.ld ) dalam editor teks dan tambahkan beberapa baris:

 SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x10000)); . = SEGMENT_START("text-segment", 0x10000) + SIZEOF_HEADERS; /* >>> Our addition. <<< */ PROVIDE(__stack_top = ORIGIN(RAM) + LENGTH(RAM)); /* >>> End of our addition. <<< */ .interp : { *(.interp) } .note.gnu.build-id : { *(.note.gnu.build-id) } 

Seperti yang Anda lihat, kami mendefinisikan __stack_top menggunakan perintah __stack_top . Simbol akan dapat diakses dari program apa pun yang terkait dengan skrip ini (dengan asumsi bahwa program itu sendiri tidak akan menentukan sesuatu dengan nama __stack_top ). Set __stack_top ke ORIGIN(RAM) . Kita tahu bahwa nilai ini adalah 0x80000000 plus LENGTH(RAM) , yaitu 128 megabytes ( 0x8000000 bytes). Ini berarti __stack_top kami disetel ke 0x88000000 .

Untuk singkatnya, saya tidak akan mencantumkan seluruh file tautan di sini , Anda dapat melihatnya di sini .

Hentikan itu! Hammertime! Runtime!


Sekarang kita memiliki semua yang kita butuhkan untuk membuat runtime C. Kita sendiri. Sebenarnya, ini adalah tugas yang cukup sederhana, di sini adalah seluruh file crt0.s :

 .section .init, "ax" .global _start _start: .cfi_startproc .cfi_undefined ra .option push .option norelax la gp, __global_pointer$ .option pop la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end 

Segera menarik sejumlah besar garis yang dimulai dengan titik. Ini adalah file untuk assembler as . Garis dengan titik-titik disebut arahan assembler : mereka memberikan informasi untuk assembler. Ini bukan kode yang dapat dieksekusi, seperti instruksi assembler RISC-V seperti jal dan add .

Mari kita pergi melalui file baris demi baris. Kami akan bekerja dengan berbagai register RISC-V standar, jadi lihat tabel ini , yang mencakup semua register dan tujuannya.

 .section .init, "ax" 

Seperti ditunjukkan dalam manual assembler GNU 'sebagai' , baris ini memberitahu assembler untuk memasukkan kode berikut ke dalam bagian .init , yang dialokasikan ( a ) dan dapat dieksekusi ( x ). Bagian ini adalah konvensi umum lainnya untuk menjalankan kode dalam sistem operasi. Kami bekerja pada perangkat keras murni tanpa OS, jadi dalam kasus kami, instruksi seperti itu mungkin tidak mutlak diperlukan, tetapi bagaimanapun juga ini adalah praktik yang baik.

 .global _start _start: 

.global membuat karakter berikut tersedia untuk ld . Tanpa ini, tautan tidak akan berfungsi, karena perintah ENTRY(_start) di skrip tautan menunjuk ke simbol _start sebagai titik masuk ke file yang dapat dieksekusi. Baris berikutnya memberi tahu assembler bahwa kita mulai mendefinisikan karakter _start .

 _start: .cfi_startproc .cfi_undefined ra ...other stuff... .cfi_endproc 

Arahan .cfi ini memberi tahu Anda tentang struktur bingkai dan cara menanganinya. .cfi_startproc dan .cfi_endproc memberi sinyal awal dan akhir fungsi, dan .cfi_undefined ra memberi tahu assembler bahwa ra register tidak boleh dikembalikan ke nilai apa pun yang dikandungnya sebelum _start .

 .option push .option norelax la gp, __global_pointer$ .option pop 

Arahan .option ini mengubah perilaku assembler sesuai dengan kode ketika Anda perlu menerapkan serangkaian opsi tertentu. Berikut adalah uraian terperinci tentang mengapa penggunaan .option di segmen ini penting:

... karena kita mungkin mengendurkan pengalamatan urutan ke urutan yang lebih pendek relatif terhadap GP, pemuatan awal GP tidak boleh dilemahkan dan harus seperti ini:

 .option push .option norelax la gp, __global_pointer$ .option pop 

sehingga setelah relaksasi Anda mendapatkan kode berikut:

 auipc gp, %pcrel_hi(__global_pointer$) addi gp, gp, %pcrel_lo(__global_pointer$) 

bukannya sederhana:

 addi gp, gp, 0 

Dan sekarang bagian terakhir dari crt0.s kami:

 _start: ...other stuff... la sp, __stack_top add s0, sp, zero jal zero, main .cfi_endproc .end 

Di sini kita akhirnya dapat menggunakan simbol __stack_top , yang telah kami kerjakan dengan susah payah untuk dibuat. The pseudo-instructions la (memuat alamat) memuat nilai __stack_top ke register sp (stack pointer), mengaturnya untuk digunakan di seluruh program.

Kemudian add s0, sp, zero menambahkan nilai register sp dan zero (yang sebenarnya merupakan register x0 dengan referensi sulit ke 0) dan menempatkan hasilnya di register s0 . Ini adalah register khusus yang tidak biasa dalam beberapa hal. Pertama, ini adalah "register persisten", yaitu disimpan ketika fungsi memanggil. Kedua, s0 terkadang bertindak sebagai frame pointer, yang memberi setiap fungsi panggilan ruang kecil di stack untuk menyimpan parameter yang dilewatkan ke fungsi ini. Cara kerja pemanggilan fungsi dengan stack dan frame pointer adalah topik yang sangat menarik yang dapat Anda gunakan dengan mudah untuk artikel terpisah, tetapi untuk sekarang, ketahuilah bahwa dalam runtime kami, penting untuk menginisialisasi penunjuk bingkai s0 .

Selanjutnya kita melihat jal zero, main pernyataan jal zero, main . Di sini, jal adalah singkatan dari Jump And Link. Instruksi mengharapkan operan dalam bentuk jal rd (destination register), offset_address . Secara fungsional, jal menulis nilai instruksi berikutnya (register pc plus empat) ke rd , dan kemudian mengatur register pc ke nilai pc saat ini ditambah alamat offset dengan ekstensi tanda , secara efektif β€œmemanggil” alamat ini.

Seperti disebutkan di atas, x0 terikat erat dengan nilai literal 0, dan menulis padanya tidak berguna.Oleh karena itu, mungkin aneh jika kita menggunakan register sebagai register tujuan zero, yang ditafsirkan oleh perakit RISC-V sebagai register x0. Bagaimanapun, ini berarti transisi tanpa syarat ke offset_address. Mengapa melakukan ini, karena di arsitektur lain ada instruksi eksplisit untuk transisi tanpa syarat?

Pola aneh jal zero, offset_addressini sebenarnya optimasi cerdas. Dukungan untuk setiap instruksi baru berarti peningkatan dan, akibatnya, kenaikan biaya prosesor. Karena itu, semakin sederhana ISA, semakin baik. Alih-alih mencemari ruang instruksi dengan dua instruksi jaldan unconditional jump, arsitektur RISC-V hanya mendukung jal, dan lompatan tanpa syarat didukung melalui jal zero, main.

RISC-V memiliki banyak optimasi seperti itu, sebagian besar mengambil bentuk yang disebut instruksi semu . Perakit tahu cara menerjemahkannya ke instruksi perangkat keras yang nyata. Misalnya, j offset_addressperakit RISC-V menerjemahkan instruksi semu untuk lompatan tanpa syarat jal zero, offset_address. Untuk daftar lengkap instruksi pseudo yang didukung secara resmi , lihat spesifikasi RISC-V (versi 2.2) .

 _start: ...other stuff... jal zero, main .cfi_endproc .end 

Baris terakhir kami adalah direktif assembler .end, yang hanya menandai akhir file.

Debug tapi sekarang nyata


Mencoba men-debug program C sederhana pada prosesor RISC-V, kami memecahkan banyak masalah. Pertama, menggunakan qemudan dtcmenemukan memori kita di mesin virtual virtRISC-V. Kemudian kami menggunakan informasi ini untuk secara manual mengontrol alokasi memori dalam versi kami dari skrip default tautan riscv64-unknown-elf-ld, yang memungkinkan kami untuk menentukan simbol secara akurat __stack_top. Kemudian kami menggunakan simbol ini dalam versi kami sendiri crt0.s, yang mengatur tumpukan kami dan petunjuk global, dan akhirnya disebut fungsi main. Sekarang Anda dapat mencapai tujuan Anda dan mulai men-debug program sederhana kami di GDB.

Ingat di sini adalah program C itu sendiri:

 cat add.c int main() { int a = 4; int b = 12; while (1) { int c = a + b; } return 0; } 

Kompilasi dan penautan:

 riscv64-unknown-elf-gcc -g -ffreestanding -O0 -Wl,--gc-sections -nostartfiles -nostdlib -nodefaultlibs -Wl,-T,riscv64-virt.ld crt0.s add.c 

Di sini kami menunjukkan lebih banyak bendera daripada yang terakhir, jadi mari kita lihat yang belum kami uraikan sebelumnya.

-ffreestanding memberitahu kompiler bahwa perpustakaan standar mungkin tidak ada , jadi tidak perlu membuat asumsi tentang keberadaan wajibnya. Parameter ini tidak diperlukan ketika memulai aplikasi pada host-nya (dalam sistem operasi), tetapi dalam hal ini tidak, oleh karena itu penting untuk menginformasikan kepada kompiler informasi ini.

-Wl- Daftar bendera yang dipisahkan koma untuk dilewati ke tautan ( ld). Di sini, ini --gc-sectionsberarti β€œbagian pengumpulan sampah”, dan lddiperintahkan untuk menghapus bagian yang tidak digunakan setelah menautkan. Tandai -nostartfiles, -nostdlibdan -nodefaultlibsberi tahu penghubung untuk tidak memproses file startup sistem standar (misalnya, defaultcrt0), implementasi standar stdlib sistem dan pustaka terkait standar sistem standar. Kami memiliki skrip crt0dan tautan kami sendiri , jadi penting untuk meneruskan bendera ini sehingga nilai default tidak bertentangan dengan preferensi pengguna kami.

-Tmenunjukkan jalur ke skrip linker kami, yang sederhana dalam kasus kami riscv64-virt.ld. Akhirnya, kami menentukan file yang ingin dikompilasi, dikompilasi, dan dikomposisikan: crt0.sdan add.c. Seperti sebelumnya, hasilnya adalah file yang lengkap dan siap dijalankan yang disebut a.out.

Sekarang jalankan executable baru kami yang sangat baru di qemu:

 # -S freezes execution of our executable (-kernel) until we explicitly tell # it to start with a 'continue' or 'c' from our gdb client qemu-system-riscv64 -machine virt -m 128M -gdb tcp::1234 -S -kernel a.out 

Sekarang jalankan gdb, ingat untuk memuat simbol debugging untuk a.out, tentukan dengan argumen terakhir:

 riscv64-unknown-elf-gdb --tui a.out GNU gdb (GDB) 8.2.90.20190228-git Copyright (C) 2019 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "--host=x86_64-apple-darwin17.7.0 --target=riscv64-unknown-elf". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out... (gdb) 

Kemudian hubungkan klien kami gdbke server gdbyang kami luncurkan sebagai bagian dari perintah qemu:

 (gdb) target remote :1234 β”‚ Remote debugging using :1234 

Atur breakpoint di main:

 (gdb) b main Breakpoint 1 at 0x8000001e: file add.c, line 2. 

Dan mulai programnya:

 (gdb) c Continuing. Breakpoint 1, main () at add.c:2 

Dari output yang diberikan, jelas bahwa kita berhasil mencapai breakpoint di jalur 2! Hal ini terbukti dalam antarmuka teks, akhirnya, kita memiliki garis yang tepat L, nilai PC:yang sama L2, dan PC:- 0x8000001e. Jika Anda melakukan semuanya seperti pada artikel, maka hasilnya akan seperti ini:



Mulai sekarang, Anda dapat menggunakannya gdbseperti biasa: -suntuk pergi ke instruksi berikutnya, info all-registersuntuk memeriksa nilai-nilai di dalam register ketika program berjalan, dll. Percobaan untuk kesenangan Anda ... kami, tentu saja , banyak bekerja untuk ini!

Apa selanjutnya


Hari ini kami telah mencapai banyak hal dan, saya harap, telah belajar banyak! Saya tidak pernah memiliki rencana formal untuk ini dan artikel-artikel berikutnya, saya hanya mengikuti apa yang paling menarik bagi saya setiap saat. Karenanya, saya tidak yakin apa yang akan terjadi selanjutnya. Saya terutama menyukai perendaman mendalam dalam instruksi jal, jadi mungkin di artikel berikutnya kita akan mengambil sebagai dasar pengetahuan yang diperoleh di sini, tetapi menggantinya dengan add.cbeberapa program di assembler RISC-V murni. Jika Anda memiliki sesuatu yang spesifik yang ingin Anda lihat atau memiliki pertanyaan, buka tiket .

Terima kasih sudah membaca! Saya berharap bertemu di artikel selanjutnya!

Opsional


Jika Anda menyukai artikel ini dan ingin tahu lebih banyak, lihat presentasi Matt Godbolt berjudul "Bits Between Bits: Bagaimana Kita Menjadi Main ()" dari konferensi CppCon2018. Dia mendekati topik sedikit berbeda dari kita di sini. Kuliah yang sangat bagus, buktikan sendiri!

Source: https://habr.com/ru/post/id454208/


All Articles