首页 > 编程语言 >RISC-V 汇编语言分析

RISC-V 汇编语言分析

时间:2023-11-15 19:12:15浏览次数:32  
标签:分析 函数 汇编语言 程序 RISC 指令 寄存器 链接

RISC-V 汇编语言分析

 

 

 3.1 导言

图 3.1 表明了从 C 程序翻译成为可以在计算机上执行的机器语言程序的四个经典步骤。 这一章的内容包括了后三个步骤,不过要从汇编语言在 RISC-V 函数调用规范中的作用 开始说起。

图 3.1 从 C 源代码翻译为可运行程序的步骤。这是从逻辑上进行的划分,实际中一些步骤会被结合起 来,加速翻译过程。在这里使用了 Unix 的文件后缀命名习惯,分别对应 MS-DOS 中 的.C, .ASM, .OBJ, .LIB 和.EXE。

3.2 函数调用规范(Calling convention) 函数调用过程通常分为 6 个阶段。

1. 将参数存储到函数能够访问到的位置;

2. 跳转到函数开始位置(使用 RV32I 的 jal 指令);

3. 获取函数需要的局部存储资源,按需保存寄存器;

4. 执行函数中的指令;

5. 将返回值存储到调用者能够访问到的位置,恢复寄存器,释放局部存储资源;

6. 返回调用函数的位置(使用 ret 指令)。 为了获得良好的性能,变量应该尽量存放在寄存器而不是内存中,但同时也要注意避免 频繁地保存和恢复寄存器,因为它们同样会访问内存。 RISC-V 有足够多的寄存器来达到两全其美的结果:既能将操作数存放在寄存器中,同 时也能减少保存和恢复寄存器的次数。其中的关键在于,在函数调用的过程中不保留部分寄 存器存储的值,称它们为临时寄存器;另一些寄存器则对应地称为保存寄存器。不再调用其 它函数的函数称为叶函数。当一个叶函数只有少量的参数和局部变量时,它们可以都被存储 在寄存器中,而不会“溢出(spilling)”到内存中。但如果函数参数和局部变量很多,程序 还是需要把寄存器的值保存在内存中,不过这种情况并不多见。

函数调用中其它的寄存器,要么被当做保存寄存器来使用,在函数调用前后值不变;要 么被当做临时寄存器使用,在函数调用中不保留。函数会更改用来保存返回值的寄存器,因 此它们和临时寄存器类似;用来给函数传递参数的寄存器也不需要保留,因此它们也类似于 临时寄存器。对于其它一些寄存器,调用者需要保证它们在函数调用前后保持不变:比如用 于存储返回地址的寄存器和存储栈指针的寄存器。图 3.2 列出了寄存器的 RISC-V 应用程序 二进制接口(ABI)名称和它们在函数调用中是否保留的规定。

 

图 3.2 RISC-V 整数和浮点寄存器的汇编助记符。RISC-V 有足够的寄存器,如果过程或方法不产生其它 调用,就可以自由使用由 ABI 分配的寄存器,不需要保存和恢复。调用前后不变的寄存器也称为“由调 用者保存的寄存器”,反之则称为“由被调用者保存的寄存器”。

根据 ABI 规范,来看看标准的 RV32I 函数入口和出口。下面是函数的开头:

 

如果参数和局部变量太多,在寄存器中存不下,函数的开头会在栈中为函数帧分配空间, 来存放。当一个函数的功能完成后,它的结尾部分释放栈帧并返回调用点:

 

很快将会看到使用这套 ABI 的一个例子,但首先需要对汇编的其它部分进行 一些解释。

3.3 汇编器

在 Unix 系统中,这一步的输入是以.s 为后缀的文件,比如 foo.s;在 MS-DOS 中则 是.ASM。 图 3.1 中的汇编器的作用不仅仅是从处理器能够理解的指令产生目标代码,还能翻译一 些扩展指令,这些指令对汇编程序员或者编译器的编写者来说通常很有用。这类指令在巧妙 配置常规指令的基础上实现,称为伪指令。图 3.3 和 3.4 列出了 RISC-V 伪指令,前者中要 求 x0 寄存器始终为 0,后者中则没有这种要求。例如,之前提到的 ret 实际上是一个伪指令, 汇编器会用 jalr x0, x1, 0 来替换它(见图 3.3)。大多数的 RISC-V 伪指令依赖于 x0。因此, 把一个寄存器硬编码为 0 便于将许多常用指令——如跳转(jump)、返回(return)、等于 0 时转移(branch on equal to zero)——作为伪指令,进而简化 RISC-V 指令集。

图 3.5 为经典的 C 程序 Hello World,编译器产生的汇编指令如图 3.6,其中使用了图 3.2 的调用规范和图 3.3、3.4 的伪指令。 汇编程序的开头是一些汇编指示符(assemble directives)。它们是汇编器的命令,具有 告诉汇编器代码和数据的位置、指定程序中使用的特定代码和数据常量等作用。

图 3.9 是 RISC-V 的汇编指示符。其中图 3.6 中用到的指示符有:

⚫ .text:进入代码段。

⚫ .align 2:后续代码按 22 字节对齐。

⚫ .globl main:声明全局符号“main”。

⚫ .section .rodata:进入只读数据段

⚫ .balign 4:数据段按 4 字节对齐。

⚫ .string “Hello, %s!\n”:创建空字符结尾的字符串。

⚫ .string “world”:创建空字符结尾的字符串。 汇编器产生如图 3.7 的目标文件,格式为标准的可执行可链接文件(ELF)格式。 按需恢复其它寄存器 恢复返回地址 释放栈帧空间 返回调用点 Hello World 程序通常 是一个新设计处理器 上运行的第一个程序。 设计者通常把能运行 操作系统并成功打印 出“Hello World”作为 新的芯片能工作的标志。

补充说明:保存寄存器和临时寄存器为什么不是连续编号的? 为了支持 RV32E——一个只有 16 个寄存器的嵌入式版本的 RISC-V(参见第 11 章),只使 用寄存器 x0 到 x15——一部分保存寄存器和一部分临时寄存器都在这个范围内。其它的保 存寄存器和临时寄存器在剩余 16 个寄存器内。RV32E 较小,但由于和 RV32I 不匹配,目前 还没有编译器支持。

 

图 3.3 依赖于 x0 的 RISC-V 伪指令。附录 A 包含了这些 RISC-V 的伪指令和真实指令。在 RV32I 中,那 些读取 64 位计数器的指令默认读取低 32 位,增加“h”时读取高 32 位。

 

图 3.4 不依赖于 x0 寄存器的 RISC-V 伪指令。在 la 指令一栏,GOT 代表全局偏移表(Global Offset Table),记录动态链接库中的符号的运行时地址。附录 A 包含了这些 RISC-V 的伪指令和真实指令。

 

图 3.5 C 语言的 Hello World 程序(hello.c)。

 

 图 3.6 RISC-V 汇编语言的 Hello World 程序(hello.s)。

 

图 3.7 RISC-V 机器语言的 Hello World 程序(hello.o)。位置 8 到 1c 这六条指令的地址字段为 0,将在后 面由链接器填充。目标文件的符号表记录了链接器所需的标签和地址。

3.4 链接器 链接器允许各个文件独立地进行编译和汇编,这样在改动部分文件时,不需要重新编译 全部源代码。链接器把新的目标代码和已经存在的机器语言模块(如函数库)等“拼接”起 来。链接器这个名字源于它的功能之一,即编辑所有对象文件的跳转并链接指令(jump and link)中的链接部分。它其实是链接编辑器(link editor)的简称,图 3.1 中的这一步骤过去 就被称为链接编辑。在 Unix 系统中,链接器的输入文件有.o 后缀,输出 a.out 文件;在 MSDOS 中输入文件后缀为.OBJ 或.LIB,输出.EXE 文件。 图 3.10 展示了一个典型的 RISC-V 程序分配给代码和数据的内存区域,链接器需要调 整对象文件的指令中程序和数据的地址,使之与图中地址相符。如果输入文件中的是与位置 无关的代码(PIC),链接器的工作量会有所降低。PIC 中所有的指令转移和文件内的数据访问都不受代码位置的影响。如第 2 章所言,RV32I 的相对转移(PC-relative branch)特性使 得程序更易于实现 PIC。 除了指令,每个目标文件还包含一个符号表,存储了程序中标签,由链接过程确定地址。 其中包括了数据标签和代码标签。图 3.6 中有两个数据标签(string1 和 string2)和两个代 码标签(main 和 printf)需要确定。由于在单个 32 位指令中很难指定一个 32 位的地址, RV32I 的链接器通常需要为每个标签调整两条指令。如图 3.6 所示:数据标签需要调整 lui 和 addi,代码标签需要调整 auipc 和 jalr。图 3.8 显示了图 3.7 中的目标文件链接后产生的 a.out 文件。

 

图 3.8 链接后的 RISC-V 机器语言 Hello World 程序。在 Unix 系统中,它的文件名是 a.out。 RISC-V 编译器支持多个 ABI,具体取决于 F 和 D 扩展是否存在。RV32 的 ABI 分别名 为 ilp32,ilp32f 和 ilp32d。ilp32 表示 C 语言的整型(int),长整型(long)和指针(pointer)都是 32 位,可选后缀表示如何传递浮点参数。在 lip32 中,浮点参数在整数寄存器中传递;在 ilp32f 中,单精度浮点参数在浮点寄存器中传递;在 ilp32d 中,双精度浮点参数也在浮点 寄存器中传递。

自然,如果想在浮点寄存中传递浮点参数,需要相应的浮点 ISA 添加 F 或 D 扩展(。因此要编译 RV32I 的代码(GCC 选项-march=rv32i),必须使用 ilp32 ABI(GCC 选项-mabi=lib32)。反过来,调用约定并不要求浮点指令一定要使用浮点寄存器,因此 RV32IFD 与 ilp32,ilp32f 和 ilp32d 都兼容。链接器检查程序的 ABI 是否和库匹配。尽管编译器本身可能支持多种 ABI 和 ISA 扩展 的组合,但机器上可能只安装了特定的几种库。因此,一种常见的错误是在缺少合适的库的 情况下链接程序。在这种情况下,链接器不会直接产生有用的诊断信息,它会尝试进行链接,然后提示不兼容。这种错误常常在从一台计算机上编译另一台计算机上运行的程序(交叉编译)时发生。

补充说明:链接器松弛(linker relaxation) 跳转并链接指令(jump and link)中有 20 位的相对地址域,因此一条指令就足够跳到很远 的位置。尽管编译器为每个外部函数的跳转都生成了两条指令,很多时候其实一条就已经足 够了。从两条指令到一条的优化同时节省了时间和空间开销,因此链接器会扫描几遍代码,尽可能地把两条指令替换为一条。每次替换会导致函数和调用它的位置之间的距离缩短,所 以链接器会多次扫描替换,直到代码不再改变。这个过程称为链接器松弛,名字来源于求解 方程组的松弛技术。除了过程调用之外,对于 gp 指针±2KiB 范围内的数据访问,RISC-V 链 接器也会使用一个全局指针替换掉 lui 和 auipc 两条指令。对 tp 指针±2KiB 范围内的线程 局部变量访问也有类似的处理。

 

图 3.9 常见 RISC-V 汇编指示符。

 

图 3.10 RV32I 为程序和数据分配内存。图中的顶部是高地址,底部是低地址。在 RISC-V 软件规范中,栈指针(sp)从 0xbffffff0 开始向下增长;程序代码段从 0x00010000 开始,包括静态链接库;程序代码 段结束后是静态数据区,在这个例子中假设从 0x10000000 开始;然后是动态数据区,由 C 语言中的 malloc()函数分配,向上增长,其中包含动态链接库。

3.5 静态链接和动态链接

上一节对静态链接(static linking)进行了说明,在程序运行前所有的库都进行了链接和 加载。如果这样的库很大,链接一个库到多个程序中会十分占用内存。另外,链接时库是绑 定的,即使它们后来的更新修复了 bug,强制的静态链接的代码仍然会使用旧的、有 bug 的 版本。 为了解决这两个问题,现在的许多系统使用动态链接(dynamic linking),外部的函数在 第一次被调用时才会加载和链接。后续所有调用都使用快速链接(fast linking),因此只会产 生一次动态开销。每次程序开始运行,它都会按照需要链接最新版本的库函数。另外,如果 多个程序使用了同一个动态链接库,库代码在内存中只会加载一次。 编译器产生的代码和静态链接的代码很相似。其不同之处在于,跳转的目标不是实际的 函数,而是一个只有三条指令的存根函数(stub function)。存根函数会从内存中的一个表中 加载实际的函数的地址并跳转。不过,在第一次调用时,表中还没有实际的函数的地址,只 有一个动态链接的过程的地址。当这个动态链接过程被调用时,动态链接器通过符号表找到 实际要调用的函数,复制到内存中,更新记录实际的函数地址的表。后续的每次调用的开销 就是存根函数的三条指令的开销。

3.6 加载器

类似图 3.8 的程序以一个可执行文件的形式存储在计算机的存储设备上。运行时,加载 器的作用是把这个程序加载到内存中,并跳转到它开始的地址。如今的“加载器”就是操作 系统。换句话说,加载 a.out 是操作系统众多的任务之一。 动态链接程序的加载稍微有些复杂。操作系统不直接运行程序,而是运行一个动态链接 器,再由动态链接器开始运行程序,并负责处理所有外部函数的第一次调用,把它们加载到 内存中,并且修改程序,填入正确的调用地址。

汇编器向 RISC-V ISA 中增加了 60 条伪指令,使得 RISC-V 代码更易于读写,并且不增 加硬件开销。将一个寄存器硬编码为 0 使得其中许多伪指令更容易实现。使用加载高位立即 数(lui)和程序计数器与高位立即数相加(auipc)两条指令,简化了编译器和链接器寻找 外部数据/函数的地址的过程。使用相对地址转移的代码与位置无关,减少了链接器的工作。 大量的寄存器减少了寄存器保存和恢复的次数,加速函数调用和返回。 RISC-V 提供了一系列简单又有影响力的机制,降低成本,提高性能,并且使得编写程 序更加容易。

体系结构研究者常用 静态链接的基准程序 来测试处理器,尽管大多数实际的程序都 有动态链接。他们说, 关心性能的用户应该 只使用静态链接,但其实这并不合理,因为加速实际的程序显 然比加速基准程序更有意义。

 

 

标签:分析,函数,汇编语言,程序,RISC,指令,寄存器,链接
From: https://www.cnblogs.com/wujianming-110117/p/17834539.html

相关文章

  • 【iOS逆向与安全】某茅台App算法分析还原
    1.目标某茅台软件的actParam算法分析还原。2.使用工具mac系统frida-ios-dump:砸壳已越狱iOS设备:脱壳及frida调试IDAPro:静态分析Charles:抓包工具ss:小火箭,配合Charles使用3.流程处理启动闪退在IDAPro搜索SVC得到如下函数列表:NOP掉sub_函数的最后一行汇编......
  • 开源埋点分析系统:洞察用户行为的新视角
    在数字化浪潮中,了解用户行为和优化用户体验成为企业竞争力的关键。对于希望深入了解客户和推动业务增长的公司来说,埋点分析系统是不可或缺的工具。今天,我们要介绍的不仅是一个ClkLog埋点分析系统,而是一种全新的、开源的洞察方案,它能够帮助您捕捉每一个关键数据点,确保您的决策基于......
  • 【课程】算法设计与分析——第八周 题解笔记
    第八周算法题解笔记1极值点题目描述给定一个单峰函数f(x)和它的定义域,求它的极值点该单峰函数f(x)保证定义域内有且只有一个极值点,且为极大值点题解本题感觉和dp关系不大,主要思路是三分法,和二分法非常类似,但没有二分法常用,主要用途是用来求单峰函数的极值对于任意一个......
  • 长安汽车基于 Apache Doris 的车联网数据分析平台建设实践
    导读:随着消费者更安全、更舒适、更便捷的驾驶体验需求不断增长,汽车智能化已成必然趋势。长安汽车智能化研究院作为长安汽车集团有限责任公司旗下的研发机构,专注于汽车智能化技术的创新与研究。为满足各业务部门的数据分析需求,长安汽车基于ApacheDoris升级了车联网数据分析平台,支......
  • 易基因:综合全基因组DNA甲基化和转录组学分析鉴定调控骨骼肌发育潜在基因 | 研究进展
    大家好,这里是专注表观组学十余年,领跑多组学科研服务的易基因。DNA甲基化是骨骼肌发育中关键的表观遗传调控机制。但胚胎鸭骨骼肌发育中负责DNA甲基化的调控因子仍然未知。2023年10月23日,南京农业大学动物科技学院于敏莉副教授团队在《IntJMolSci》杂志发表题为“TheIntegrat......
  • 记一次线上问题引发的对 Mysql 锁机制分析
    背景最近双十一开门红期间组内出现了一次因Mysql死锁导致的线上问题,当时从监控可以看到数据库活跃连接数飙升,导致应用层数据库连接池被打满,后续所有请求都因获取不到连接而失败整体业务代码精简逻辑如下:@Transactionpublicvoidservice(Integerid){delete(id);......
  • 数据分析科学家的角色和重要性
    数据分析科学家的角色和重要性角色概述数据分析科学家的主要角色是从复杂的数据集中提取有价值的见解。这涉及数据的收集、清理、分析和解释。他们利用统计学、数据挖掘和机器学习技术来预测趋势、识别数据模式,并为决策提供数据支持的洞察。重要技能统计学和机器学习:理解和应......
  • 智能装备生产线数据分析平台有什么功能
    在智能装备产线,各种设备的状态与生产数据实时反映了工厂的生产效率与产能水平,需要实时监控并实现可视化数据分析,从而有效提升生产交付能力,打造高效流畅的生产工序,是建设数字化智能工厂的重要内容之一。 数之能提供的工业数据分析平台将数据共享于工业制造流程的每一个环节,利用数据......
  • 记一次线上问题引发的对 Mysql 锁机制分析 | 京东物流技术团队
    背景最近双十一开门红期间组内出现了一次因Mysql死锁导致的线上问题,当时从监控可以看到数据库活跃连接数飙升,导致应用层数据库连接池被打满,后续所有请求都因获取不到连接而失败整体业务代码精简逻辑如下:@Transactionpublicvoidservice(Integerid){delete(id);inse......
  • 学生考试成绩分析报告怎么写?
    引言本报告旨在对学生的考试成绩进行详细分析,揭示学生的学习情况和潜在问题,并提供建议以促进学生的学业发展。本次分析基于[学校名称]的某一学期的考试成绩数据,该学期共计[总人数]名学生参与考试。本报告将依次从整体情况、各科成绩分布、优秀学生和需关注的学生群体等方面进行......