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!、退出当前进程:

gas 复制代码
	.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 R un 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等符号:

gas 复制代码
	.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代码编译生成的,并非笔者手动编写:

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开发汇编应用的两种方法。第一种是使用riscvecall汇编指令,直接调用Linux内核提供的系统调用;这种方法生成的可执行文件是静态链接的,不依赖动态链接器也不能调用标准C语言库提供的功能。第二种方法是可以连接到标准C语言库的,而且是动态链接的。当然,第二种方法也可以方便地扩展到其他动态库,而不仅限于标准C语言库。笔者对比了两种方法,发现第二种可以方便地访问到应用运行时的命令行参数,那么如何使用第一种方法访问这些命令行参数呢?

通过查看全志D1的Linux内核代码可知,应用的命令行参数及环境变量是存放在应用的栈上面的:

c 复制代码
/* 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
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全部代码,仅供参考:

gas 复制代码
	.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的程序。

相关推荐
许白掰1 小时前
Linux入门篇学习——Linux 工具之 make 工具和 makefile 文件
linux·运维·服务器·前端·学习·编辑器
longze_75 小时前
Ubuntu连接不上网络问题(Network is unreachable)
linux·服务器·ubuntu
Dirschs5 小时前
【Ubuntu22.04安装ROS Noetic】
linux·ubuntu·ros
qianshanxue115 小时前
ubuntu 操作记录
linux
AmosTian8 小时前
【系统与工具】Linux——Linux简介、安装、简单使用
linux·运维·服务器
这我可不懂11 小时前
Python 项目快速部署到 Linux 服务器基础教程
linux·服务器·python
车车不吃香菇11 小时前
java idea 本地debug linux服务
java·linux·intellij-idea
tan77º11 小时前
【Linux网络编程】Socket - TCP
linux·网络·c++·tcp/ip
代码改变世界ctw11 小时前
ARM汇编编程(AArch64架构)课程 - 第5章函数调用规范
汇编·arm开发·架构
kfepiza12 小时前
Linux的`if test`和`if [ ]中括号`的取反语法比较 笔记250709
linux·服务器·笔记·bash