在全志RISCV/D1设备上安装汇编器和链接器
去年笔者将openwrt-22.03
系统移植到了基于全志D1/riscv64的嵌入式设备上。当时发现系统启动后,网络不可用;简单地修改/etc/config/network
设备即可以正常连接有线网络。为了学习riscv ISA
,笔者手动为该设备编译了汇编器链接器(不含gcc编译器)、GNU make
以及Vim,这样就可以在全志D1嵌入式设备上学习riscv的汇编语言开发了。因这些工具是手动编译生成的,其安装路径如下:
root@OpenWrt:/tmp# uname -a
Linux OpenWrt 5.4.61+ #0 PREEMPT Sun Jul 31 15:12:47 2022 riscv64 GNU/Linux
root@OpenWrt:/tmp# ls /opt/binutils/bin/
ar ld objdump tic
as ld.bfd ranlib toe
captoinfo less readelf tput
clear lessecho reset tset
dmesg lesskey rview view
ex make rvim vim
file ncurses6-config strings vimdiff
infocmp nm strip vimtutor
infotocap objcopy tabs xxd
root@OpenWrt:/tmp# echo $PATH
/opt/binutils/bin:/usr/bin:/usr/sbin:/bin:/sbin
简单的helloword汇编示例
本着由简入繁的学习原则,笔者编写了一个不依赖glibc库的简单汇编代码,其编译运行的调试结果如下:
root@OpenWrt:/tmp/assembly# make helloworld
as -mabi=lp64d -fPIC -o helloworld.o helloworld.S
ld --eh-frame-hdr -melf64lriscv -e _pentry -o helloworld helloworld.o
root@OpenWrt:/tmp/assembly# file helloworld
helloworld: ELF 64-bit LSB executable, UCB RISC-V, double-float ABI, version 1 (SYSV), statically linked, not stripped
root@OpenWrt:/tmp/assembly# ./helloworld
Hello World!
root@OpenWrt:/tmp/assembly# echo $?
3
注意到,上面生成的helloworld
是一个静态链接的可执行文件,它不需要动态链接器。helloworld.S
是一个入门级的汇编代码(对于riscv的汇编代码学习,除了riscv官方提供的ISA详解文档外,笔者还推荐github上的一个汇总说明文档),它参考了Linux内核关于syscall系统调用的说明(其中riscv相关的内容),分别使用ecall
汇编指令调用了write
及exit
两个系统调用,分别用于给柡准输出写Hello World!
、退出当前进程:
.file "helloworld.S".option pic.text.section .text.startup,"ax",@progbits.align 4.globl _pentry.type _pentry, @function_pentry:li a7, 64li a0, 1lla a1, .LC0li a2, 13ecallnopli a7, 93li a0, 3ecallnop.size _pentry, . - _pentry.section .rodata.str,"aMS",@progbits,1.align 4
.LC0:.string "Hello World!\n"
在RISCV64嵌入式设备上编译运行调用glibc的汇编应用
上面的简单helloworld
应用,并不依赖glibc
的C语言动态库。这一约束大大限制了我们在全志D1/riscv嵌入式设备上编写的汇编应用的功能。而该设备上也缺少整套的gcc
工具链,如何解决这一问题?笔者的方案是将链接到glibc
的C语方库的这一过程整合到全志D1设备上运行,就可以直接在汇编代码中引用柡准C语言库提供的变量及函数。举例说明,对于一个依赖C语言库的示例应用example.S
,在全志D1设备上的汇编、链接过程如下:
root@OpenWrt:/tmp/assembly# make example
as -mabi=lp64d -fPIC -o example.o example.S
ld --eh-frame-hdr -melf64lriscv -dynamic-linker \/lib/ld-linux-riscv64xthead-lp64d.so.1 \-o example /tmp/assembly/lib64xthead-lp64d/crt1.o \/tmp/assembly/lib64xthead-lp64d/crti.o \/tmp/assembly/lib64xthead-lp64d/crtbegin.o \-L/tmp/assembly/lib64xthead-lp64d -L/lib \example.o --no-as-needed -lc --no-as-needed \/tmp/assembly/lib64xthead-lp64d/crtend.o \/tmp/assembly/lib64xthead-lp64d/crtn.o
root@OpenWrt:/tmp/assembly# file example
example: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-riscv64xthead-lp64d.so.1, for GNU/Linux 4.15.0, with debug_info, not stripped
root@OpenWrt:/tmp/assembly# ldd ./examplelinux-vdso.so.1 (0x0000003ff6200000)libc.so.6 => /lib64xthead/lp64d/libc.so.6 (0x0000003ff60f2000)/lib/ld-linux-riscv64xthead-lp64d.so.1 (0x0000003ff6202000)
root@OpenWrt:/tmp/assembly# ./example 5 6
Hello World!
Value A: 5, B: 6, result: 30
需要说明的是,在使用C语言库编写汇编应用时,需要严格地遵守riscv-abi中要求的调用规则,这里不再展开。上面链接过程中需要用到的多个目柡文件crtX.o
,是在PC机上从交叉编译器riscv64-glibc-gcc-thead_20200702.tar.xz
中提取的(其中,libc.so
是一个修改后的文本文件)。这些文件源于glibc
,用于Linux系统环境下应用的C运行时(C Run Time)的初始化操作:
root@OpenWrt:/tmp/assembly# ls lib64xthead-lp64d/
crt1.o crtend.o crtn.o libc_nonshared.a
crtbegin.o crti.o libc.so rv64-ld-log.txt
root@OpenWrt:/tmp/assembly# cat lib64xthead-lp64d/libc.so
/* GNU ld scriptUse the shared library, but some functions are only inthe static library, so try that secondarily. */
OUTPUT_FORMAT(elf64-littleriscv)
GROUP ( /lib/libc.so.6 /tmp/assembly/lib64xthead-lp64d/libc_nonshared.a AS_NEEDED ( /lib/ld-linux-riscv64xthead-lp64d.so.1 ) )
以下给出调用C库的一些函数的汇编代码example.S
源文件,它引用了libc.so.6
动态库中的stdout
、fprintf
、strtoll
等符号:
.file "example.c".option pic.text.section .text.startup,"ax",@progbits.align 1.align 4.globl main.type main, @function
main:addi sp,sp,-48sd s0,8(sp)sd s1,16(sp)sd s2,24(sp)sd ra,40(sp)sd s3,32(sp)addi s0,sp,48li a5,1li s2,0li s1,0bgt a0,a5,.L7
.L2:mul a4,s1,s2la s3,stdoutld a0,0(s3)mv a3,s1mv a2,s2lla a1,.LC0call fprintf@pltld a0,0(s3)call fflush@pltld ra,40(sp)ld s0,8(sp)subw a0,s2,s1ld s3,32(sp)ld s1,16(sp)ld s2,24(sp)addi sp,sp,48jr ra
.L7:mv s1,a0ld a0,8(a1)mv s3,a1li a2,0li a1,0call strtoll@pltli a5,2mv s2,a0beq s1,a5,.L4ld a0,16(s3)li a2,0li a1,0call strtoll@pltmv s1,a0j .L2
.L4:li s1,0j .L2.size main, .-main.section .rodata.str1.8,"aMS",@progbits,1.align 3
.LC0:.string "Hello World!\nValue A: %lld, B: %lld, result: %lld\n"
值得说明的是,上面的example.S
是由以下C代码编译生成的,并非笔者手动编写:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>int main(int argc, char * argv[])
{long long aval, bval;aval = bval = 0;if (argc >= 2)aval = (long long) strtoll(argv[1], NULL, 0);if (argc >= 3)bval = (long long) strtoll(argv[2], NULL, 0);fprintf(stdout, "Hello World!\nValue A: %lld, B: %lld, result: %lld\n",aval, bval, aval * bval);fflush(stdout);return (int) (aval - bval);
}
Linux内核执行应用层可执行文件时的命令行参数传递
以上笔者演示了在嵌入式设备上,仅使用汇编器as
及链接器ld
开发汇编应用的两种方法。第一种是使用riscv
的ecall
汇编指令,直接调用Linux内核提供的系统调用;这种方法生成的可执行文件是静态链接的,不依赖动态链接器也不能调用标准C语言库提供的功能。第二种方法是可以连接到标准C语言库的,而且是动态链接的。当然,第二种方法也可以方便地扩展到其他动态库,而不仅限于标准C语言库。笔者对比了两种方法,发现第二种可以方便地访问到应用运行时的命令行参数,那么如何使用第一种方法访问这些命令行参数呢?
通过查看全志D1的Linux内核代码可知,应用的命令行参数及环境变量是存放在应用的栈上面的:
/* awd1-linux-5.4/fs/exec.c */
/** 'copy_strings()' copies argument/environment strings from the old* processes's memory to the new process's stack. The call to get_user_pages()* ensures the destination page is created and not swapped out.*/
static int copy_strings(int argc, struct user_arg_ptr argv,struct linux_binprm *bprm)
{struct page *kmapped_page = NULL;char *kaddr = NULL;unsigned long kpos = 0; int ret; while (argc-- > 0) { const char __user *str;int len; unsigned long pos; ret = -EFAULT;str = get_user_arg_ptr(argv, argc);if (IS_ERR(str))goto out;
......
static int do_execveat_common(int fd, struct filename *filename, ... {
......retval = copy_string_kernel(bprm->filename, bprm);if (retval < 0)goto out_free;bprm->exec = bprm->p;retval = copy_strings(bprm->envc, envp, bprm);if (retval < 0)goto out_free;retval = copy_strings(bprm->argc, argv, bprm);if (retval < 0)goto out_free;
}
于是,笔者编写了不依赖C语言库及其运行时的稍复杂一些的汇编代码dumpenv.S
,它会遍历函数栈上的保存的应用命令行参数及环境变量,依次输出到标准输出,下面是编译运行的结果:
root@OpenWrt:/tmp/assembly# make dumpenv
as -mabi=lp64d -fPIC -o dumpenv.o dumpenv.S
ld --eh-frame-hdr -melf64lriscv -e _pentry -o dumpenv dumpenv.o
root@OpenWrt:/tmp/assembly# file dumpenv
dumpenv: ELF 64-bit LSB executable, UCB RISC-V, double-float ABI, version 1 (SYSV), statically linked, not stripped
root@OpenWrt:/tmp/assembly# ./dumpenv hello world "" Welcome To RISC-V
stackpointer: 0x0000003ffffc3c80
stkptr[0x00]: 0x0000000000000007
stkptr[0x08]: 0x0000003ffffc3e7d
stkptr[0x10]: 0x0000003ffffc3e87
stkptr[0x18]: 0x0000003ffffc3e8d
./dumpenv
hello
worldWelcome
To
RISC-V
USER=root
SSH_CLIENT=192.168.1.8 49510 22
SHLVL=1
HOME=/root
OLDPWD=/tmp
SSH_TTY=/dev/pts/0
SSH_PUBKEYINFO=xiaoqzye@163.com
PS1=\[\e]0;\u@\h: \w\a\]\u@\h:\w\$
ENV=/etc/shinit
LOGNAME=root
TERM=xterm
PATH=/opt/binutils/bin:/usr/bin:/usr/sbin:/bin:/sbin
SHELL=/bin/ash
PWD=/tmp/assembly
SSH_CONNECTION=192.168.1.8 49510 192.168.1.6 22
./dumpenv
有人会问,为什么在环境变量结束后,还会有一个./dumpenv
的信息?这一点可以参考上面帖出的Linux内核源码:因在riscv平台上,C语言函数栈是向下生长的,内核在栈上构造这些信息时,参数的写入恰好与dumpenv
输出的顺序是相反的。在调用copy_strings
存入环境变量之前,会单独将应用的可执行文件名先写入,即以上调试结果的最后一个./dumpenv
。下面是笔者编写的dumpenv.S
全部代码,仅供参考:
.file "dumpenv.S".option pic.text.section .text.startup, "ax", @progbits.align 4.globl dump_int.type dump_int, @function
dump_int:addi sp, sp, -48addi a1, sp, 8mv a2, a1li a3, 0x30sb a3, 0x0(a2)addi a2, a1, 1li a3, 0x78sb a3, 0x0(a2)li a4, 2li a7, 0x39
1:li a5, 17sub a5, a5, a4sll a5, a5, 2srl a5, a0, a5andi a5, a5, 0xfaddi a3, a5, 0x30bleu a3, a7, 2faddi a3, a3, 0x27
2:add a5, a1, a4sb a3, 0x0(a5)addi a4, a4, 1li a5, 18bne a4, a5, 1bli a2, 18li a0, 1li a7, 64ecallnopaddi sp, sp, 48ret.size dump_int, . - dump_int.align 4.globl dump_str.type dump_str, @function
dump_str:addi sp, sp, -16sd zero, 0x8(sp)beqz a0, 1fli a2, -1
2:addi a2, a2, 1add a1, a0, a2lbu a3, 0x0(a1)bnez a3, 2bbeqz a2, 1fsd a2, 0x8(sp)mv a1, a0li a0, 1li a7, 64ecall
1:nopld a0, 0x8(sp)addi sp, sp, 16ret.size dump_str, . - dump_str.align 4.globl dump_char.type dump_char, @function
dump_char:addi sp, sp, -16sb a0, 8(sp)addi a1, sp, 8li a2, 1li a0, 1li a7, 64ecallnopaddi sp, sp, 16ret.size dump_char, .-dump_char.align 4.global dump_str_array.type dump_str_array, @function
dump_str_array:addi sp, sp, -48bnez a0, 1faddi sp, sp, 48ret
1:sd s0, 0x8(sp)sd s1, 0x10(sp)sd s2, 0x18(sp)sd ra, 0x20(sp)mv s0, a0mv s1, a12:bgtz s1, 3flbu a0, 0x0(s0)beqz a0, 4f
3:mv a0, s0call dump_straddi s2, a0, 1li a0, 10call dump_charadd s0, s0, s2addi s1, s1, -1j 2b4:ld s0, 0x8(sp)ld s1, 0x10(sp)ld s2, 0x18(sp)ld ra, 0x20(sp)addi sp, sp, 48ret.size dump_str_array, . - dump_str_array.align 4.globl _pentry.type _pentry, @function
_pentry:mv s4, spaddi sp, sp, -16lla a0, .Lstkptrcall dump_strmv a0, s4call dump_intli a0, 10call dump_charlla a0, .Lsp_0call dump_strld a0, 0x0(s4)call dump_intli a0, 10call dump_charlla a0, .Lsp_8call dump_strld a0, 0x8(s4)call dump_intli a0, 10call dump_charlla a0, .Lsp_10call dump_strld a0, 0x10(s4)call dump_intli a0, 10call dump_charlla a0, .Lsp_18call dump_strld a0, 0x18(s4)call dump_intli a0, 10call dump_charld a0, 0x8(s4)ld a1, 0x0(s4)call dump_str_arrayli a7, 93li a0, 8ecallnop.size _pentry, . - _pentry.section .rodata.str, "aMS", @progbits, 1.align 4
.Lstkptr:.string "stackpointer: \0"
.Lsp_0:.string "stkptr[0x00]: \0"
.Lsp_8:.string "stkptr[0x08]: \0"
.Lsp_10:.string "stkptr[0x10]: \0"
.Lsp_18:.string "stkptr[0x18]: \0"
至此我们可以说,基于riscv
的应用层汇编的开发,有了一个可行的方法。但若要深入了解riscv ISA
,仅编写应用层汇编是不够的,还需要能够编写、运行Supervisor
模式下的需要优先级的汇编指令;这一点,可以通过修改u-boot
或Linux内核源码来学习;有时间和精力的,也可以从头写一个BareMetal
的程序。