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
- Apa itu RISC-V?
- Mengkonfigurasi Alat QEMU dan RISC-V
- Hai RISC-V!
- Pendekatan naif
- Mengangkat tirai -v
- Cari tumpukan kami
- Tata letak
- Hentikan itu!
Hammertime! Runtime!
- Debug tapi sekarang nyata
- Apa selanjutnya
- 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:
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
.
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
.
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.
Ini menciptakan file
a.out
, yang
gcc
default ke file yang dapat dieksekusi. Sekarang jalankan file ini di
qemu
:
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:
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
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:
- UI teks tidak dapat menemukan sumbernya. Antarmuka harus menampilkan kode kami dan breakpoints terdekat.
- GDB tidak melihat garis eksekusi saat ini (
L??
) dan menampilkan penghitung 0x0 ( PC: 0x0
).
- 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).
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.
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
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_address
ini 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 jal
dan 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_address
perakit 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 qemu
dan dtc
menemukan memori kita di mesin virtual virt
RISC-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-sections
berarti βbagian pengumpulan sampahβ, dan ld
diperintahkan untuk menghapus bagian yang tidak digunakan setelah menautkan. Tandai -nostartfiles
, -nostdlib
dan -nodefaultlibs
beri tahu penghubung untuk tidak memproses file startup sistem standar (misalnya, defaultcrt0
), implementasi standar stdlib sistem dan pustaka terkait standar sistem standar. Kami memiliki skrip crt0
dan tautan kami sendiri , jadi penting untuk meneruskan bendera ini sehingga nilai default tidak bertentangan dengan preferensi pengguna kami.-T
menunjukkan jalur ke skrip linker kami, yang sederhana dalam kasus kami riscv64-virt.ld
. Akhirnya, kami menentukan file yang ingin dikompilasi, dikompilasi, dan dikomposisikan: crt0.s
dan 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
:
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 gdb
ke server gdb
yang 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 gdb
seperti biasa: -s
untuk pergi ke instruksi berikutnya, info all-registers
untuk 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.c
beberapa 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!