diff --git a/examples/hello-riscv/README.md b/examples/hello-riscv/README.md new file mode 100644 index 0000000..59368e4 --- /dev/null +++ b/examples/hello-riscv/README.md @@ -0,0 +1,61 @@ +# Hello World in RISC-V + +## 64-bit + +Assemble: + +```sh +riscv64-elf-as -march rv64i -mabi lp64 -o hello.o hello.s +``` + +Link: + +```sh +riscv64-elf-ld -o exe --verbose hello.o +``` + +Execute the RISC-V ELF in QEMU: + +```sh +qemu-riscv64 exe +``` + +Run a query on the RISC-V ELF: + +```sh +poetry run sqlelf examples/hello-riscv/exe \ + --sql "SELECT * FROM elf_instructions LIMIT 5;" +``` + +Or run a query and attach a debugger: + +```sh +poetry run sqlelf-debug examples/hello-riscv/exe \ + --sql "SELECT * FROM elf_instructions LIMIT 5;" +``` + +Double check the disassembly: + +```sh +riscv64-elf-objdump --disassemble examples/hello-riscv/exe +``` + +## 32-bit + +Assemble `hello.s` into an object file for the RISC-V 32-bit base integer instruction set (`-march rv32i`), little-endian (`-mlittle-endian`), with an ABI that follows the convention where `int`, `long` and `pointer` types are all 32-bit, with debug symbols included in the object file (`-g`): + +```sh +riscv64-elf-as -march rv32i -mabi ilp32 -mlittle-endian -o hello.o hello.s -g +``` + +Link the object file into a RISC-V 32-bit little-endian executable (`-m elf32lriscv`), with the symbol `_start` as its entry point: + +```sh +riscv64-elf-ld -e _start -m elf32lriscv -o exe --verbose hello.o +``` + +Execute the RISC-V ELF in QEMU: + +```sh +qemu-riscv32 exe +``` diff --git a/examples/hello-riscv/hello.s b/examples/hello-riscv/hello.s new file mode 100644 index 0000000..b0c98c5 --- /dev/null +++ b/examples/hello-riscv/hello.s @@ -0,0 +1,41 @@ +.section .text +.globl _start +.equ STDOUT, 1 # File descriptor 1 is standard output (stdout) +.equ WRITE, 64 # Linux write syscall +.equ EXIT, 93 # Linux exit syscall +.equ EXIT_CODE_SUCCESS, 0 + +_start: + # In C, a list of parameters is passed to the kernel in a certain sequence. + # For the write system call, the parameters are structured as follows: + # ssize_t write(int fd, const void *buf, size_t count) + # The three parameters passed are: + # 1. a file descriptor (e.g. 1 for stdout) + # 2. a pointer to a character buffer (i.e. a string) + # 3. the number of characters in that string to be written. + li a0, STDOUT + la a1, buf_begin + # Load a byte from memory, zero-pad it (to a 64-bit value in RV64), and store + # the unsigned value in the destination register a2. + lbu a2, buf_size + + # Store the system call number in register a7. + li a7, WRITE + # Switch to RISC-V supervisor mode (the Linux kernel runs in this mode) and + # make a request using the value stored in a7 as the system call number. + ecall + + li a0, EXIT_CODE_SUCCESS + li a7, EXIT + ecall + +# The .rodata section of an ELF binary contains constant values. The .rodata +# section is marked as read-only, so these values cannot change at runtime. +.section .rodata + +buf_begin: + .string "Hello World!\n" +buf_size: + # Current address (the .) minus address of buf_begin = length of buffer. + # We store the result in a 8-bit word using the .byte directive. + .byte .-buf_begin