无题
ISA和RISC-V
ISA
ISA是Instruction Set Architecture(指令集体系架构)缩写,指令集是一个计算机系统支持的所有机器指令的集合,常看作软硬件之间的分界面。指令集对上限定了软件的基本功能,对下制定了硬件实现的功能目标。
ISA指令集架构是底层硬件电路层面向上层软件程序提供的一层接口规范。
定义了: 基本数据类型、寄存器、指令、寻址模式、异常或者中断的处理方式
ISA的宽度指的是CPU中的通用寄存器的宽度,这决定了寻址范围的大小、以及数据运行运算的能力。ISA的宽度和指令编码长度无关
ISA可分为CISC和RISC。CISC是指复杂指令系统计算机,RISC是指精简指令系统计算机。
CISC:通过设置一些功能复杂的指令,把一些原来由软件实现的、常用的功能改用硬件指令实现,以此来提高计算机的执行速度。越来越多的复杂指令被加人指令系统中,逐渐形成了一个庞大且复杂的指令集。
目标是强化指令功能,减少程序的指令条数,达到提高性能的目的。
X86是现存唯一的CISC指令集。
RISC:尽量简化计算机指令功能,只保留那些功能简单、能在一个节拍内执行完成的指令,而把较复杂的功能用段子程序来实现。 RISC通过减少指令种类、规范指令格式和简化寻址方式等方法,方便处理器内部的并行处理,提高超大规模集成电路(VLSI)器件的使用效率,从而大幅度地提高处理器的性能。
RISC 指令系统仅包含最常用的简单指令,因此可以通过硬件优化设计,把时钟频率提得很高,从而实现整个系统的高性能。同时,RISC 技术在 CPU 芯片上设置大量寄存器,用来保存常用的数据,以大大减少对存储器的访问,用高速的寄存器访问取代低速的存储器访问,从而提高系统整体性能。
RISC-V是一种基于RISC原则开源的ISA。
特点:简单、清晰的分层设计、模块化、稳定、社区化
ISA命令格式:RV[##][abc..xyz]
RV是指用于标识RISC-V体系架构的前缀,[##]标识处理器的宽度,[abc..xyz]支持什么模块。
模块化ISA:由一个基础整数指令集+多个可选的扩展指令集。
基本指令集 | 描述 |
---|---|
RV32I | 32位整数指令集 |
RV32E | RV32I的子集,用于小型的嵌入式场景 |
RV64I | 64位整数指令集,兼容RV32I |
RV128I | 128整数指令集,兼容RV64I和RV32I |
扩展指令集 | 描述 |
---|---|
M | 整数乘法与除法指令集 |
A | 存储器原子指令集 |
F | 单精度(32bit)浮点指令集 |
D | 双精度(64bit)浮点指令兼容F |
C | 压缩(Compressed)指令集 |
…… | 其他标准化和为标准化的指令集 |
特定组合IMAFD被称为通用组合用字母G表示。
HART:指令执行流。
RISC-V
RISC-V的特权级别(Privileged Specification)定义了三个特权级别(privilege level)
machine级别是最高的级别,所有的实现都需要支持。
level | Encoding | Name | Abbreviation |
---|---|---|---|
0 | 00 | User/Application | U |
1 | 01 | Supervisor | S |
2 | 10 | Reserved | |
3 | 11 | Machine | M |
不同的特权级别下时分别对应各自的一套Register(CSR),用于控制和获取对应level下的处理器工作状态。高级别的特权级别可以访问低级别的CSR,反之不行。 |
内存管理保护
物理内存保护允许M模式指定u模式可以访问内存地址,支持R/W/X以及Lock。
编译和链接
GCC
常用选项 | 含义 |
---|---|
-E | 只做预处理 |
-c | 只编译不链接,生产目标文件’.o’’ |
-S | 生成汇编代码 |
-o file | 把输出生成到由file指定文件名的文件中 |
-g | 在输出的文件中加入支持调试的信息 |
-v | 显示输出详细的命令执行过程信息 |
编译:预处理指处理源文件中的#开头的预处理指令,编译则针对预处理的结果进行一系列的词汇分析、语法分析、语义分析,优化后生成的汇编指令,存放在.o为后缀的目标文件中。
汇编:汇编器将汇编语言转为机器可以执行的命令。
链接:链接器将汇编器生成的目标文件和一些标准库(譬如libc)文件组合,形成最终可执行的应用程序。
ELF
ELF是一种Unix-like系统上的二进制文件格式标准。
ELF标准中定义的采用ELF格式分四类:
可重定位文件:内容包含了代码和数据,可以被链接成可执行文件或共享目标文件。(Linux上的.o文件)
可执行文件:可以直接执行的程序(Linux上的a.out)
共享目标文件:内容包含了代码和数据,可以作为链接器的输入,在链接阶段和其他的Relocatable File或者Shared Object File 一起链接成新的Object File,或者运行阶段作为动态链接器的输入,和Executable File结合,作为进程的一部分来运行(Linux上的.so)
核心存储文件:进程意外终止时,系统可以将该进程的部分内容和终止时的其他状态信息保存到该文件中以供调试分析。(Linux上的core文件)
ELF文件处理相关工具:Binutils(http://www.gnu.org/software/binutils/)
ar:归档文件,将多个文件打包成一个大文件。
as:被gcc调用,输入汇编文件,输出目标文件供链接器Id链接
Id:GNU链接器。被gcc调用,它把目标文件和各种库文件结合在一起,重定位数据,并链接符号引用。
objcopy:执行文件格式转换。
objdump:显示ELF文件的信息。
readelf:显示更多EFL格式文件的信息
读取一个hello_world.o的文件头信息
1 | gcc hello_world -o |
RISC-V架构常见gcc编译选项
1.-march=RISCV_ARCH
- 用于告诉编译器目标芯片的架构情况,生成对应的二进制代码
- 比如:-march=rv32im
- rv32:告诉编译器生成的代码实在RISC-V架构的32位芯片上运行,就不会使用ld和sd命令,而使用lw和sw
- im:芯片支持i指令集和m指令集
2.-mabi=RISC_ABI
数据类型 | int | long | 指针 |
---|---|---|---|
ilp32/ilp32f/ilp32d | 32bit | 32bit | 32bit |
lp64/lp64f/lp64d | 32bit | 64bit | 64bit |
浮点数传参规则 | 需要支持的浮点指令扩展 | float参数 | double参数 |
---|---|---|---|
ilp32/lp64 | 不需要 | 通过整数寄存器(a0-a1)传递 | 通过整数寄存器(a0-a3)传递 |
ilp32f/lp64f | 需要支持F扩展 | 通过浮点寄存器(fa0-fa1)传递 | 通过整数寄存器(a0-a3)传递 |
ilp32d/lp64d | 需要支持F和D扩展 | 通过浮点寄存器(fa0-fa1)传递 | 通过浮点寄存器(fa0-fa1)传递 |
-mabi参数用于指定编译目标的adi,RISC-V定义了两个整型abi和三个浮点abi。
两个整型abi
ilp32:int、long、pointer都是32位,long long是64位,char是8位,short是16位
lp64:long、pointer是64位,int是32位,其他类型和ilp32一样三个浮点abi
“”空字符:在寄存器中不传递浮点参数
f:32位浮点寄存器,需要支持F扩展
d:64位浮点寄存器,需要支持F和D扩展
3.-mlittle-endian、-mbig-endian
4.-mcmodel
-mcmodel=medlow:只能寻址4G,对于32位架构芯片刚好,但是64位的芯片则不能寻址全部空间。
-mcmodel=medany: 寻址范围在当前PC的前后2G范围,PC是不断变化的,可以实现把64位架构芯片地址全部寻址。
模拟器QEMU
QEMU是一套以GPL许可证分发源码的计算机系统模拟软件。支持架构:ARM,MIPS,AMD,RISC等等。
QEMU两种主要模式:user mode和system mode。
QEMU的下载与安装
1 | sudo apt install python3-venv |
RISCV的工具链下载与安装
1 | #使用vpn下载更快 |
GDB工具的使用
GDB的PDF网址
使用GDB调试qemu下的riscv64-linux-gnu-gcc编译的程序
1 |
|
编译调试
1 | riscv64-linux-gnu-gcc -g hello.c -o hello |
Linker Script链接脚本
链接器一般都提供多种控制整个链接过程的方法,以用来产生用户所须要的文件。
一般链接器有如下三种方法:
使用命令行来给链接器指定参数,ld的-o、-e参数就属于这类。
将链接指令存放在目标文件里面,编译器经常会通过这种方法向链接器传递指令。方法也比较常见,只是我们平时很少关注,比如VISUAL C++编译器会把链接参数放在PE目标文件的.drectve段以用来传递参数。
使用链接控制脚本。
绝大部分情况下,我们使用链接器提供的默认链接规则对目标文件进行链接。
在编译普通的应用程序时,可以使用默认的链接器脚本,但是对于内核程序来说,它本身也是一个.elf文件,这个.elf文件该怎么组织,各个段放到内存中什么地方,这个由于和底层硬件强相关,所以需要我们自己编写相关的链接器脚本。
在C代码中直接获取链接器脚本中定义的符号是有一定的限制的。C语言是一种静态编译语言,在编译时会将源代码转换为机器码,并生成可执行文件。链接器脚本用于指导链接器如何组织可执行文件的各个部分,包括代码段、数据段、符号表等。
代码中的符号表是编译器在编译过程中使用的数据结构,用于管理程序中的变量、函数和其他标识符的信息。
在C代码中,无法直接引用链接器脚本中定义的符号的值,因为C编译器并不了解链接器脚本的细节。C编译器只能根据给定的C代码进行编译,将代码转换为机器码,并生成符号表。符号表中包含了在C代码中定义的全局变量、函数等符号及其对应的地址。
要在C代码中获取链接器脚本中定义的符号的值,一种常见的做法是通过在C代码中声明外部变量,并使用链接器脚本中定义的符号来初始化这些外部变量。这样,链接器在链接阶段会将外部变量与链接器脚本中定义的符号关联起来,并将符号的值赋给外部变量。然后,C代码就可以通过访问这些外部变量来获取链接器脚本中定义的符号的值。
总之,C代码无法直接获取链接器脚本中定义的符号的值,但可以通过声明外部变量并与符号关联来间接获取。这种间接的方式使得C代码能够与链接器脚本进行交互,并共享符号的值。