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, 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动态库中的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,-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开发汇编应用的两种方法。第一种是使用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
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的程序。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/172843.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MongoDB使用$literal获取表达式字面值

系统开发过程中&#xff0c;会有保存表达式的需求。比如保存数字或字符的字段拼接规则等。开发人员期望读取或保存这些规则时&#xff0c;不要被插入数据库或从数据库中读取规则时执行。或者保存模版时&#xff0c;也不希望讲模版中的表达式计算出结果。 mongodb的$literal方法…

点云 surface 方法总结

点云的表面方法是指通过点云数据来估计和重建物体或场景的表面几何形状。下面总结了几种常见的点云表面方法&#xff1a; 三角化&#xff1a;三角化是最常用的点云表面重建方法之一。它将点云中的点连接成三角形网格&#xff0c;从而重建出物体或场景的表面。常见的三角化算法…

基于Loki + Promtail + Grafana 搭建 Nginx 日志监控

文章目录 引言第一部分&#xff1a;Loki 简介与安装1.1 Loki 简介1.2 Loki 安装1.2.1 下载 Loki1.2.2 安装 Loki 1.3 启动 Loki 第二部分&#xff1a;Promtail 简介与安装2.1 Promtail 简介2.2 Promtail 安装2.2.1 下载 Promtail2.2.2 安装 Promtail 2.3 启动 Promtail 第三部分…

231126 刷题日报

1. 高楼扔鸡蛋 O(N*logN) 2. 698. 划分为k个相等的子集 没做出来&#xff0c;和划分两个子集不同 3. 300. 最长递增子序列 LIS petencie sorting 没看懂&#xff0c;明天看吧 4. 518. 零钱兑换 II 完全背包问题&#xff1a;每个物品数量是无限的 注意&#xff1a;dp的定义…

【刷新:重新发现商业与未来】书笔记

收获 同理心&#xff1a;站在他人角度考虑他人感受&#xff0c;他人需要什么&#xff0c;我能提供什么&#xff1b;他人可以是员工&#xff0c;家人等&#xff1b;对于员工来讲核心四件事&#xff1a;1、薪水&#xff1b;2、有结果&#xff1b;3、有成长&#xff1b;4、工作开…

openssl + ECDH + linux+开发详解(C++)

一、什么是ECDH ECDH&#xff08;Elliptic Curve Diffie-Hellman&#xff09;是一种基于椭圆曲线密码学的密钥交换协议&#xff0c;用于在通信双方之间安全地协商共享密钥。ECDH是Diffie-Hellman密钥交换协议的一种变体&#xff0c;它利用椭圆曲线上的离散对数问题&#xff0c…

微服务系列(三)--通过spring cloud zuul过滤器实现线上流量复制

思路 补充一下&#xff0c;为什么这里我会想到使用"pre"类型的过滤器实现流量复制/流量镜像。 刚开始的时候&#xff0c;参考了阿里的流量镜像实现方案&#xff1a; 配置流量复制策略&#xff0c;阿里的方案本身是对基于云原生envoy做的&#xff0c;这确实是istio原…

关于嵌入式系统一些名词的小结(ARM/CORTEX/STM32等)

Microcontroller和Microprocessor啥区别&#xff1f; Microcontroller通常包括CPU和外设在一起&#xff0c;CPU通常计算能力没那么强&#xff0c;而且比较便宜。 Microprocessor通常只包括一个计算能力很强的CPU&#xff0c;比较贵。它会连到其它外设。 CPU ARM 是ARM公司开发…

使用不平衡数据集练习机器学习

一、介绍 在当今世界&#xff0c;机器学习和人工智能几乎被广泛应用于每个领域&#xff0c;以提高绩效和结果。但如果没有数据&#xff0c;它们还有用吗&#xff1f;答案是否定的。机器学习算法严重依赖我们提供给它们的数据。我们提供给算法的数据质量在很大程度上决定了机器学…

2023年第十六届山东省职业院校技能大赛中职组“网络安全”赛项竞赛正式试题

第十六届山东省职业院校技能大赛中职组 “网络安全”赛项竞赛试题 目录 一、竞赛时间 二、竞赛阶段 三、竞赛任务书内容 &#xff08;一&#xff09;拓扑图 &#xff08;二&#xff09;A模块基础设施设置/安全加固&#xff08;200分&#xff09; &#xff08;三&#xf…

Centos 7.9 Install Docker Insecure Registry

文章目录 1. 镜像存储规划2. 安装定制 docker3. 部署 registry4. 验证镜像仓库 1. 镜像存储规划 linux LVM /dev/sdb mount dir /data【linux LVM 磁盘挂载目录】 创建两个目录 一个 docker 数据存储目录 &#xff1a;/data/docker&#xff0c;默认一般为linux为 /var/lib/d…

boomYouth

周一&#xff1a; 1. action异步写法&#xff1a; <script setup> import sonCom1 from /components/sonCom1.vue import sonCom2 from /components/sonCom2.vue import {useCountStore} from /store/counter import {useChannelStore} from /store/channel const count…

力扣打卡 1410-HTML实体解析器

Problem: 1410. HTML 实体解析器 思路 当处理 HTML 实体解析时&#xff0c;先构建一个映射&#xff0c;将特殊字符实体与它们的替换字符对应起来。 接下来&#xff0c;使用迭代的方法遍历输入的文本字符串。当遇到&字符时&#xff0c;开始检查可能的字符实体。 如果找到了…

1、nmap常用命令

文章目录 1. 主机存活探测2. 常见端口扫描、服务版本探测、服务器版本识别3. 全端口&#xff08;TCP/UDP&#xff09;扫描4. 最详细的端口扫描5. 三种TCP扫描方式&#xff08;1&#xff09;TCP connect 扫描&#xff08;2&#xff09;TCP SYN扫描&#xff08;3&#xff09;TCP …

PTA-7-55 判断指定字符串是否合法

题目&#xff1a; 输入一个字符串&#xff0c;判断指定字符串是否合法&#xff0c;要求字符串由7个字符组成&#xff0c;并且第一位必须是大写字母&#xff0c;2-4为必须是小写字母&#xff0c;后3为必须是数字字符&#xff0c;要求使用正则表达式来实现。 根据题目要求&#x…

防火墙命令行基础配置实验(H3C模拟器)

嘿&#xff0c;这里是目录&#xff01; ⭐ H3C模拟器资源链接1. 实验示意图2. 要求3. 当前配置3.1 PC配置3.2 FW配置&#xff08;防火墙&#xff09;[^7][^8]3.2.1 FW1配置3.2.2 FW2配置 3.3 R配置3.3.1 R1配置3.3.2 R2配置 3.4 SW配置3.4.1 SW1配置3.4.2 SW2配置3.4.3 SW3配置…

1.9 字符数组

1.9 字符数组 一、字符数组概述二、练习 一、字符数组概述 所谓字符数组&#xff0c;就是char类型的数组&#xff0c;比如 char a[]&#xff0c;是C语言中最常用的数组类型&#xff0c;先看一个程序 #include <stdio.h> #define MAXLINE 1000 //最大行长度限制 int get…

gradle构建项目速度优化及排查方式

文章目录 一、前言二、Android项目优化1、相关配置2、构建速度分析 三、Gradle项目通用优化1、分析构建耗时2、使用配置进行优化3、优化依赖解析a. 避免不必要和未使用的依赖项b. 优化存储库顺序 c. 最小化动态和快照版本d. 通过构建扫描查找动态和变化的版本e. 通过构建扫描可…

⑤【Sorted Set】Redis常用数据类型: ZSet [使用手册]

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ 目录 ⑤Redis Zset 操作命令汇总1. zadd 添加或…

[C/C++]数据结构 堆的详解

一:概念 堆通常是一个可以被看做一棵完全二叉树的数组对象,它是一颗完全二叉树,堆存储的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并且需要满足每个父亲结点总小于其子节点(或者每个父亲结点总大于其子节点) 堆可以分为两种: 小堆: 任意一个父亲节点都小于其子…