RISCV汇编与Linux内核传参

在全志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汇编指令调用了writeexit两个系统调用,分别用于给柡准输出写Hello World!、退出当前进程:

	.file	"helloworld.S"
	.option pic

	.text
	.section .text.startup,"ax",@progbits
	.align	4
	.globl _pentry
	.type _pentry, @function

_pentry:
	li a7, 64
	li a0, 1
	lla	a1, .LC0
	li a2, 13
	ecall
	nop

	li a7, 93
	li a0, 3
	ecall
	nop
	.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 ./example
	linux-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 script
   Use the shared library, but some functions are only in
   the 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动态库中的stdoutfprintfstrtoll等符号:

	.file	"example.c"
	.option pic
	.text
	.section	.text.startup,"ax",@progbits
	.align	1
	.align	4
	.globl	main
	.type	main, @function
main:
	addi	sp,sp,-48
	sd	s0,8(sp)
	sd	s1,16(sp)
	sd	s2,24(sp)
	sd	ra,40(sp)
	sd	s3,32(sp)
	addi	s0,sp,48
	li	a5,1
	li	s2,0
	li	s1,0
	bgt	a0,a5,.L7
.L2:
	mul	a4,s1,s2
	la	s3,stdout
	ld	a0,0(s3)
	mv	a3,s1
	mv	a2,s2
	lla	a1,.LC0
	call	fprintf@plt
	ld	a0,0(s3)
	call	fflush@plt
	ld	ra,40(sp)
	ld	s0,8(sp)
	subw	a0,s2,s1
	ld	s3,32(sp)
	ld	s1,16(sp)
	ld	s2,24(sp)
	addi	sp,sp,48
	jr	ra
.L7:
	mv	s1,a0
	ld	a0,8(a1)
	mv	s3,a1
	li	a2,0
	li	a1,0
	call	strtoll@plt
	li	a5,2
	mv	s2,a0
	beq	s1,a5,.L4
	ld	a0,16(s3)
	li	a2,0
	li	a1,0
	call	strtoll@plt
	mv	s1,a0
	j	.L2
.L4:
	li	s1,0
	j	.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 
#include 
#include 

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开发汇编应用的两种方法。第一种是使用riscvecall汇编指令,直接调用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
world

Welcome
To
RISC-V
USER=root
SSH_CLIENT=192.168.1.8 49510 22
SHLVL=1
HOME=/root
OLDPWD=/tmp
SSH_TTY=/dev/pts/0
[email protected]
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, -48
	addi a1, sp, 8

	mv a2, a1
	li a3, 0x30
	sb a3, 0x0(a2)

	addi a2, a1, 1
	li a3, 0x78
	sb a3, 0x0(a2)

	li a4, 2
	li a7, 0x39
1:
	li a5, 17
	sub a5, a5, a4
	sll a5, a5, 2
	srl a5, a0, a5
	andi a5, a5, 0xf
	addi a3, a5, 0x30
	bleu a3, a7, 2f
	addi a3, a3, 0x27
2:
	add a5, a1, a4
	sb a3, 0x0(a5)
	addi a4, a4, 1
	li a5, 18
	bne a4, a5, 1b

	li a2, 18
	li a0, 1
	li a7, 64
	ecall
	nop

	addi sp, sp, 48
	ret
	.size dump_int, . - dump_int

	.align 4
	.globl dump_str
	.type dump_str, @function
dump_str:
	addi sp, sp, -16
	sd zero, 0x8(sp)
	beqz a0, 1f
	li a2, -1
2:
	addi a2, a2, 1
	add a1, a0, a2
	lbu a3, 0x0(a1)
	bnez a3, 2b
	beqz a2, 1f

	sd a2, 0x8(sp)
	mv a1, a0
	li a0, 1
	li a7, 64
	ecall
1:
	nop
	ld a0, 0x8(sp)
	addi sp, sp, 16
	ret
	.size dump_str, . - dump_str

	.align 4
	.globl dump_char
	.type dump_char, @function
dump_char:
	addi sp, sp, -16
	sb a0, 8(sp)
	addi a1, sp, 8
	li a2, 1
	li a0, 1
	li a7, 64
	ecall
	nop
	addi sp, sp, 16
	ret
	.size dump_char, .-dump_char

	.align 4
	.global dump_str_array
	.type dump_str_array, @function
dump_str_array:
	addi sp, sp, -48
	bnez a0, 1f
	addi sp, sp, 48
	ret
1:
	sd s0, 0x8(sp)
	sd s1, 0x10(sp)
	sd s2, 0x18(sp)
	sd ra, 0x20(sp)
	mv s0, a0
	mv s1, a1

2:
	bgtz s1, 3f
	lbu a0, 0x0(s0)
	beqz a0, 4f
3:
	mv a0, s0
	call dump_str
	addi s2, a0, 1
	li a0, 10
	call dump_char
	add s0, s0, s2
	addi s1, s1, -1
	j 2b

4:
	ld s0, 0x8(sp)
	ld s1, 0x10(sp)
	ld s2, 0x18(sp)
	ld ra, 0x20(sp)
	addi sp, sp, 48
	ret
	.size dump_str_array, . - dump_str_array

	.align 4
	.globl _pentry
	.type _pentry, @function
_pentry:
	mv s4, sp
	addi sp, sp, -16

	lla a0, .Lstkptr
	call dump_str
	mv a0, s4
	call dump_int
	li a0, 10
	call dump_char

	lla a0, .Lsp_0
	call dump_str
	ld a0, 0x0(s4)
	call dump_int
	li a0, 10
	call dump_char

	lla a0, .Lsp_8
	call dump_str
	ld a0, 0x8(s4)
	call dump_int
	li a0, 10
	call dump_char

	lla a0, .Lsp_10
	call dump_str
	ld a0, 0x10(s4)
	call dump_int
	li a0, 10
	call dump_char

	lla a0, .Lsp_18
	call dump_str
	ld a0, 0x18(s4)
	call dump_int
	li a0, 10
	call dump_char

	ld a0, 0x8(s4)
	ld a1, 0x0(s4)
	call dump_str_array

	li a7, 93
	li a0, 8
	ecall
	nop
	.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的程序。

你可能感兴趣的:(汇编,linux,risc-v)