安装RISCV工具链
riscv-gnu-toolchain工具链和模拟器安装记录 - 知乎 (zhihu.com)
riscv-gnu-toolchain工具链分elf-gcc、linux-gnu-gcc两个版本,以及对应的32位和64位版本。两个版本的主要区别是:
- riscv32-unknown-elf-gcc、riscv64-unknown-elf-gcc使用的是riscv-newlib库(面向嵌入式的C库),只支持静态链接,不支持动态链接。
- riscv32-unknown-linux-gnu-gcc、riscv64-unknown-linux-gnu-gcc使用的是glibc标准库,支持动态链接。
最终选用的是riscv64-unknown-linux-gnu-gcc,支持动态链接
函数调用约定
x86
- 调用约定:
- 常见的调用约定包括
cdecl
、stdcall
和fastcall
。 - 不同的调用约定通常是为了满足特定编程语言或平台的需求。
- cdecl(C Declaration):调用者清理堆栈,支持可变参数,C语言标准推荐使用。
- stdcall:被调用者清理堆栈,不支持可变参数,通常用于 Windows API。
- fastcall:使用寄存器传递前两个参数(ECX 和 EDX),其余参数通过堆栈传递,适用于函数频繁调用的场景。
平台与工具链
不同的编译器和平台可能会选择不同的调用约定。
例如,Microsoft 的 Visual C++ 编译器通常支持 stdcall 和 fastcall,
而 GCC 支持 cdecl 和其他约定。
- 参数传递:参数从右到左入栈。
- 返回值:返回值通常通过 EAX 寄存器返回。
- 通用寄存器:8 个(EAX, EBX, ECX, EDX, ESI, EDI, EBP, ESP)
- 指令指针寄存器:1 个(EIP)
- 段寄存器:6 个(CS, DS, SS, ES, FS, GS)
- 总数:大约 15 个(包含所有类型)
x64
- 调用约定:
- 常用的调用约定为 Microsoft 和 System V(Linux 和 macOS)。
- Microsoft x64(Window操作系统,常与WindowsAPI 交互):
-
- 前四个整型或指针参数通过寄存器(RCX、RDX、R8、R9)传递,其余通过堆栈。
- 返回值通过 RAX 寄存器返回。
- System V x64( Linux /Unix操作系统 , 常与 POSIX 标准库和开源项目兼容。 ):
-
- 前六个整型或指针参数通过寄存器(RDI、RSI、RDX、RCX、R8、R9)传递,其余通过堆栈。
- 返回值通过 RAX 寄存器返回。
- 参数传递:同样是从右到左入栈。
- 通用寄存器:16 个(RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8, R9, R10, R11, R12, R13, R14, R15)
- 指令指针寄存器:1 个(RIP)
- 总数:17 个(包含所有类型)
RV32(RV64类似,但支持更大的寄存器范围)
- 调用约定:
- 使用 RISC-V 的 ABI,函数参数通过寄存器和堆栈传递。
- 参数传递:
- 前八个参数通过寄存器(a0 到 a7)传递。
- 如果参数超过八个,其余参数通过堆栈传递。
- 返回值:
- 返回值通过 a0 寄存器(即 x10)返回。
调用约定
函数调用过程通常分为 6 个阶段 [Patterson and Hennessy 2021]:
- 将参数存放到函数可访问的位置;
- 跳转到函数入口(使用 RV32I 的 jal 指令);
- 获取函数所需局部存储资源,按需保存寄存器;
- 执行函数功能;
- 将返回值存放到调用者可访问的位置,恢复寄存器,释放局部存储资源;
- 由于程序可从多处调用函数,故需将控制权返回到调用点(使用 ret 指令)
为提升性能,应尽量将变量存放在寄存器而不是内存中,但同时也要避免因保存
和恢复寄存器(函数调用和上下文切换)而频繁访问内存。
RISCV架构优势在于
1.足量的寄存器
2.可以减少保存和恢复寄存器的次数
寄存器的分类
1.临时寄存器:不保证其值在函数调用前后的一致性
2.保存寄存器:保证其值在函数调用前后的一致性
叶子函数
不再调用其他函数的函数称为叶子函数。
当一个叶子函数只有少量参数和局部变量时,可将其分配到寄存器, 无需分配到内存。大部分函数调用均如此,此时程序无需将寄存器保存到内存
RISCV寄存器
比较特殊的几个寄存器
- 裸机环境
在裸机(bare-metal)编程中,如果程序没有操作系统,tp 通常不会被使用。
如果程序需要自己实现多线程,则可以定义一个线程控制块,并通过软件设置 tp 指向相应的结构。
在函数入口处和出口处标准的RV32I代码
entry_label:
addi sp,sp,-framesize # 调整栈指针(sp 寄存器)来分配栈帧
sw ra,framesize-4(sp) # 保存返回地址(ra 寄存器)
# 按需保存其他寄存器
... # 函数体
# 按需恢复其他寄存器
lw ra,framesize-4(sp) # 恢复返回地址寄存器
addi sp,sp, framesize # 释放栈帧空间
ret # 返回调用点
汇编器
汇编器的作用
1.面向处理器:生成的目标代码(处理器可以理解的指令)
2.面向程序员和编译器开发者:伪指令(常规指令的巧妙特例)
RISCV中的伪指令分为两类
1.依赖恒为0的x0寄存器(硬连线为0)——————>极大简化了RISCV指令集
电气连接:在电路设计中,硬连线意味着某个信号线直接连接到地(0V),从而使得该信号始终保持在低电平状态(即逻辑 0)。
2.其他
伪指令
1.依赖恒为0的x0寄存器的伪指令(32条)
set not equal to zero
set less than zero
set greater than zero
将rs与x0寄存器的值(0)进行条件比较,满足比较条件将rd置为1,否则为0
看大于0时置位,实则是用slt指令进行比较的,无sgt指令
branch equal to zero
以第一条为例,比较rs寄存器的值是否为0,为0则跳转到当前指令相对偏移offset地址处
jump and link 无条件跳转
1.jal x0, offset
会直接跳转到目标地址
适用于不需要保存返回地址的跳转,offset
:是一个相对地址偏移量,指向要跳转到的目标指令地址
(正常情况下在进行函数调用时,需要将调用函数处的下一条指令的地址保存在寄存器中,此处寄存器为x0,硬连线为0, 这种跳转适用于不需要返回的场景,如错误处理或实现循环)
2.jalr x0, rs, 0
会直接跳转到 rs
中存储的地址,
3. 同上,唯一区别是返回地址寄存器x1:
在 RISC-V 架构中,x1
寄存器通常被称为 ra
(return address)寄存器,专门用于存储返回地址。 此处代表从子过程返回
什么是尾调用?
以C源代码为例,将return func();成为尾调用
用jmp 跳转指令替换掉call+ret组合的指令成为尾调用优化
尾调用优化,用jmp 跳转指令替换掉call+ret组合的指令成为尾调用优化,减少栈帧的维护
尾调用优化可以减少函数中不必要的 调用流程
时间上:更少更快的指令
空间上:更少的栈帧,不再容易产生overflow的问题,减少内存中需要维护的栈帧数量
编译器/解释器支持尾调用优化
clang 、gcc对尾调用优化都有很好的支持
javaScript在Webkit引擎上可以进行适当的尾调用优化
不支持尾调用优化
两条指令如何理解呢?在 RISC-V 中,auipc
和 jalr
指令可以用来实现远距离的尾调用(tail call)操作。让我们逐条分析这两条指令的功能以及它们在尾调用中的角色。
AUIPC
和 JALR
里的 12 位立即数的组合,能够转移控制到任意的 32 位 PC 相对地址
AUIPC
先把 32位立即数的高 20 位,补上低 12 位全 0跟 PC 相加,把结果存到某个寄存器里
JALR
再把这个寄存器和 32 位立即数的低 12 位相加,跳转到相加得到的目标地址。
也就是实现了 32 位立即数加上 PC的目标地址
三个相关的CSR寄存器
1.instret
:这是一个 CSR,表示已提交指令的数量。它是一个 64 位的寄存器(在 RV64 中),可以分为高 32 位和低 32 位,分别是 instret[63:32]
和 instret[31:0]
,通常通过 instret[h]
访问高 32 位,通过 instret
访问低 32 位。
2.cycle
:这是一个 CSR,表示 CPU 时钟周期数。它也是一个 64 位寄存器,可以分为高 32 位和低 32 位,分别是 cycle[63:32]
和 cycle[31:0]
。通过 cycle[h]
访问高 32 位。
3.time
:这是一个 CSR,表示当前实时时间。与前两者一样,它是一个 64 位寄存器,可以分为高 32 位和低 32 位,分别是 time[63:32]
和 time[31:0]
。通过 time[h]
访问高 32 位。
这三条指令的主要作用是读取 RISC-V 处理器的性能计数器,便于进行性能分析和监控。通过这些寄存器,开发者可以获取有关 CPU 执行的指令数量、时钟周期和当前时间的信息。这对于优化代码和评估系统性能非常重要
CSR置位详细解释:
- 操作:
csr = csr | rs
- 目的:
-
- 通过将
rs
中为 1 的位与csr
中对应的位进行按位或操作,来设置csr
中的某些位。 csrrs
指令用于启用某些功能或状态,例如启用特定的中断或设置状态位
- 通过将
CSR清位详细解释:
- 操作:
csr = csr & ~rs
- 目标寄存器:
x0
是目标寄存器,但由于它是硬连线为 0,所以指令执行后不会保存csr
的原始值。 - 功能:该指令会从
csr
中清除与rs
中为 1 的位对应的位。这意味着csr
中被清零的位是根据rs
中的位设置决定的
浮点的舍入模式
FRM 寄存器值与舍入模式对应关系
FRM 值 | 舍入模式 | 说明 |
0 | Round to Nearest (RN) | 最近舍入:向最接近的可表示值舍入。如果恰好在两个值之间,则向偶数舍入(即取最接近的偶数)。 |
1 | Round Toward Zero (RZ) | 向零舍入:直接截断小数部分,向零方向舍入。 |
2 | Round Toward Positive Infinity (RP) | 向上舍入:总是向正无穷舍入,适用于希望结果偏大时的场景。 |
3 | Round Toward Negative Infinity (RN) | 向下舍入:总是向负无穷舍入,适用于希望结果偏小时的场景。 |
浮点异常标志
浮点异常标志(FFLAGS)
标志位 | 名称 | 说明 |
0 | Invalid Operation (IV) | 无效操作:指示执行了无效的浮点操作,例如除以零或无效的浮点数。 |
1 | Division by Zero (DZ) | 除以零:指示发生了除以零的操作。 |
2 | Overflow (OV) | 溢出:指示浮点运算结果超出了可以表示的范围。 |
3 | Underflow (UF) | 下溢:指示浮点运算结果过小,低于可表示的最小正数(非零)。 |
4 | Inexact (NX) | 不精确:指示运算结果不是精确的结果(例如舍入发生)。 |
在 RV32I 中,那些读取 64 位计数器的指令默认读取低 32 位,可通过 “h” 结尾的指令
读取高 32 位。
在 RV32I 架构中,64 位计数器通常指的是用来记录处理器执行周期、已执行指令数量或时间的特定寄存器。例如:
- Cycle Counter:用于跟踪 CPU 时钟周期的数量,帮助进行性能分析。
- Instruction Retired Counter:记录已经执行的指令数量。
这些计数器通常被实现为 64 位,以便能够记录长时间运行程序所产生的计数值。但在 RV32I 的 32 位环境中,默认读取操作只涉及到这类寄存器的低 32 位部分。要读取高 32 位部分,则需要使用带有 “h” 后缀的特殊指令。这种设计允许处理器在性能监控方面提供更大的灵活性和更精确的计数。
2.其他伪指令(28条)
fence iorw, iorw
的作用是确保在执行此指令之前的所有 I/O 操作和内存读写操作都已经完成,且在此指令之后的 I/O 和内存操作在屏障之前的操作完成后才会执行。这种屏障对于多处理器或多线程环境尤为重要,可以防止因操作顺序不当而导致的竞态条件或数据不一致性。
程序实例
1.源代码
#include <stdio.h>
int main()
{
printf("Hello, %s\n", "world");
return 0;
}
2.编译生成汇编程序Hello.s
riscv64-unknown-linux-gnu-gcc -S Hello.c -o Hello.s
-S
选项告诉编译器将 C 程序编译为汇编代码,输出文件为Hello.s
。-o Hello.s
指定输出文件名。
.file "Hello.c"
.option nopic
.attribute arch, "rv64i2p1_m2p0_a2p1_f2p2_d2p2_c2p0_zicsr2p0"
.attribute unaligned_access, 0
.attribute stack_align, 16
.text
.section .rodata
.align 3
.LC0:
.string "world"
.align 3
.LC1:
.string "Hello, %s\n"
.text
.align 1
.globl main
.type main, @function
main:
.LFB0:
.cfi_startproc
addi sp,sp,-16
.cfi_def_cfa_offset 16
sd ra,8(sp)
sd s0,0(sp)
.cfi_offset 1, -8
.cfi_offset 8, -16
addi s0,sp,16
.cfi_def_cfa 8, 0
lui a5,%hi(.LC0)
addi a1,a5,%lo(.LC0)
lui a5,%hi(.LC1)
addi a0,a5,%lo(.LC1)
call printf
li a5,0
mv a0,a5
ld ra,8(sp)
.cfi_restore 1
ld s0,0(sp)
.cfi_restore 8
.cfi_def_cfa 2, 16
addi sp,sp,16
.cfi_def_cfa_offset 0
jr ra
.cfi_endproc
.LFE0:
.size main, .-main
.ident "GCC: () 13.2.0"
.section .note.GNU-stack,"",@progbits
汇编指示符:
形式:以英文句号开头
作用:通知汇编器在何处放置代码和数据,指定程序中使用的代码和数据常量
在以上程序中可能出现的汇编指示符,如下所示:
常见的汇编指示符
3.汇编生成目标程序Hello.o
riscv64-unknown-linux-gnu-gcc -c Hello.s -o Hello.o
-c
选项告诉编译器只进行编译而不进行链接,输出一个目标文件 Hello.o
。
riscv64-unknown-linux-gnu-objdump -d Hello.o
汇编器生成如下所示的 ELF(Executable and Linkable Format,可执行可链接格式) 标准格式目标文件
Hello.o: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <main>:
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
8: 000007b7 lui a5,0x0
c: 00078593 mv a1,a5
10: 000007b7 lui a5,0x0
14: 00078513 mv a0,a5
18: 00000097 auipc ra,0x0
1c: 000080e7 jalr ra # 18 <main+0x18>
#如上六条指令的地址字段为0,后续由链接器填充
20: 4781 li a5,0
22: 853e mv a0,a5
24: 60a2 ld ra,8(sp)
26: 6402 ld s0,0(sp)
28: 0141 addi sp,sp,16
2a: 8082 ret
除指令外,每个目标文件还包含一张符号表,用于记录程序中所有需要在链接过程中确定地址的符号
其中包含数据符号和代码符号。
riscv64-unknown-linux-gnu-objdump -t Hello.o
Hello.o: file format elf64-littleriscv
SYMBOL TABLE:
0000000000000000 l df *ABS* 0000000000000000 Hello.c
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .rodata 0000000000000000 .rodata
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 l d .comment 0000000000000000 .comment
0000000000000000 l d .eh_frame 0000000000000000 .eh_frame
0000000000000000 l d .riscv.attributes 0000000000000000 .riscv.attributes
#代码符号
0000000000000000 g F .text 000000000000002c main
0000000000000000 *UND* 0000000000000000 printf
数据符号(如字符串常量):常量字符串(如 "Hello, %s\n"
和 "world"
)被存储在 .rodata
段中,通常没有单独的符号名,符号表只会标记这些段存在,而不会为每个常量字符串分配单独的符号
oslab@oslab-virtual-machine:/opt/riscv/bin$ riscv64-unknown-linux-gnu-objdump -s -j .rodata Hello.o
Hello.o: file format elf64-littleriscv
Contents of section .rodata:
0000 776f726c 64000000 48656c6c 6f2c2025 world...Hello, %
0010 730a00
生成PIC位置无关的目标文件
使用-fPIC编译选项
riscv64-unknown-linux-gnu-gcc -fPIC -c Hello.c -o Hello_pic.o
riscv64-unknown-linux-gnu-objdump -d Hello_pic.o
oslab@oslab-virtual-machine:/opt/riscv/bin$ riscv64-unknown-linux-gnu-objdump -d Hello_pic.o
Hello_pic.o: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <main>:
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
8: 00000597 auipc a1,0x0
c: 00058593 mv a1,a1
10: 00000517 auipc a0,0x0
14: 00050513 mv a0,a0
18: 00000097 auipc ra,0x0
1c: 000080e7 jalr ra # 18 <main+0x18>
20: 4781 li a5,0
22: 853e mv a0,a5
24: 60a2 ld ra,8(sp)
26: 6402 ld s0,0(sp)
28: 0141 addi sp,sp,16
2a: 8082 ret
riscv64-unknown-linux-gnu-gcc -pie -o Hello_pic Hello_pic.o
4.链接生成可执行文件Hello.out
riscv64-unknown-linux-gnu-gcc Hello.o -o Hello.out
riscv64-unknown-linux-gnu-objdump -d Hello.out
Hello.out: file format elf64-littleriscv
Disassembly of section .plt:
0000000000010430 <_PROCEDURE_LINKAGE_TABLE_>:
10430: 97 23 00 00 33 03 c3 41 03 be 03 bc 13 03 43 fd .#..3..A......C.
10440: 93 82 03 bc 13 53 13 00 83 b2 82 00 67 00 0e 00 .....S......g...
0000000000010450 <__libc_start_main@plt>:
10450: 00002e17 auipc t3,0x2
10454: bb0e3e03 ld t3,-1104(t3) # 12000 <__libc_start_main@GLIBC_2.34>
10458: 000e0367 jalr t1,t3
1045c: 00000013 nop
0000000000010460 <printf@plt>:
10460: 00002e17 auipc t3,0x2
10464: ba8e3e03 ld t3,-1112(t3) # 12008 <printf@GLIBC_2.27>
10468: 000e0367 jalr t1,t3
1046c: 00000013 nop
Disassembly of section .text:
0000000000010470 <_start>:
10470: 024000ef jal 10494 <load_gp>
10474: 87aa mv a5,a0
10476: 00000517 auipc a0,0x0
1047a: 01c50513 addi a0,a0,28 # 10492 <__wrap_main>
1047e: 6582 ld a1,0(sp)
10480: 0030 addi a2,sp,8
10482: ff017113 andi sp,sp,-16
10486: 4681 li a3,0
10488: 4701 li a4,0
1048a: 880a mv a6,sp
1048c: fc5ff0ef jal 10450 <__libc_start_main@plt>
10490: 9002 ebreak
0000000000010492 <__wrap_main>:
10492: a89d j 10508 <main>
0000000000010494 <load_gp>:
10494: 00002197 auipc gp,0x2
10498: 37c18193 addi gp,gp,892 # 12810 <__global_pointer$>
1049c: 8082 ret
...
00000000000104a0 <deregister_tm_clones>:
104a0: 6549 lui a0,0x12
104a2: 6749 lui a4,0x12
104a4: 01050793 addi a5,a0,16 # 12010 <__TMC_END__>
104a8: 01070713 addi a4,a4,16 # 12010 <__TMC_END__>
104ac: 00f70863 beq a4,a5,104bc <deregister_tm_clones+0x1c>
104b0: 00000793 li a5,0
104b4: c781 beqz a5,104bc <deregister_tm_clones+0x1c>
104b6: 01050513 addi a0,a0,16
104ba: 8782 jr a5
104bc: 8082 ret
00000000000104be <register_tm_clones>:
104be: 6549 lui a0,0x12
104c0: 01050793 addi a5,a0,16 # 12010 <__TMC_END__>
104c4: 6749 lui a4,0x12
104c6: 01070593 addi a1,a4,16 # 12010 <__TMC_END__>
104ca: 8d9d sub a1,a1,a5
104cc: 4035d793 srai a5,a1,0x3
104d0: 91fd srli a1,a1,0x3f
104d2: 95be add a1,a1,a5
104d4: 8585 srai a1,a1,0x1
104d6: c599 beqz a1,104e4 <register_tm_clones+0x26>
104d8: 00000793 li a5,0
104dc: c781 beqz a5,104e4 <register_tm_clones+0x26>
104de: 01050513 addi a0,a0,16
104e2: 8782 jr a5
104e4: 8082 ret
00000000000104e6 <__do_global_dtors_aux>:
104e6: 1141 addi sp,sp,-16
104e8: e022 sd s0,0(sp)
104ea: 6449 lui s0,0x12
104ec: 01844783 lbu a5,24(s0) # 12018 <completed.0>
104f0: e406 sd ra,8(sp)
104f2: e791 bnez a5,104fe <__do_global_dtors_aux+0x18>
104f4: fadff0ef jal 104a0 <deregister_tm_clones>
104f8: 4785 li a5,1
104fa: 00f40c23 sb a5,24(s0)
104fe: 60a2 ld ra,8(sp)
10500: 6402 ld s0,0(sp)
10502: 0141 addi sp,sp,16
10504: 8082 ret
0000000000010506 <frame_dummy>:
10506: bf65 j 104be <register_tm_clones>
0000000000010508 <main>:
10508: 1141 addi sp,sp,-16
1050a: e406 sd ra,8(sp)
1050c: e022 sd s0,0(sp)
1050e: 0800 addi s0,sp,16
在第一段代码中,a1 和 a0 寄存器的值是通过 lui 和 addi 指令计算的,
偏移量是常量(1336 和 1344),计算的是某个绝对地址附近的值。
10510: 67c1 lui a5,0x10
10512: 53878593 addi a1,a5,1336 # 10538 <_IO_stdin_used+0x8>
10516: 67c1 lui a5,0x10
10518: 54078513 addi a0,a5,1344 # 10540 <_IO_stdin_used+0x10>
1051c: f45ff0ef jal 10460 <printf@plt>
在第二段位置无关(PIC)代码中,a1 和 a0 寄存器的值是通过 auipc 和 addi 来构造的,
使用的是相对地址(相对于当前代码位置的偏移)
这意味着在生成的位置无关代码中,程序的地址依赖于当前指令的位置,
而不是固定的常量地址。
690: 00000597 auipc a1,0x0
694: 02858593 addi a1,a1,40 # 6b8 <_IO_stdin_used+0x8>
698: 00000517 auipc a0,0x0
69c: 02850513 addi a0,a0,40 # 6c0 <_IO_stdin_used+0x10>
6a0: f21ff0ef jal 5c0 <printf@plt>
10520: 4781 li a5,0
10522: 853e mv a0,a5
10524: 60a2 ld ra,8(sp)
10526: 6402 ld s0,0(sp)
10528: 0141 addi sp,sp,16
1052a: 8082 ret
链接器
动态链接
1.plt表和got表
2.延迟绑定
在RISCV中Linux动态链接和延迟绑定
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
return 0;
}
依次用以下的编译命令,现在有原有的 test.c 还有个 test.o 以及可执行文件 test
riscv64-unknown-linux-gnu-gcc -Wall -g -o test.o -c test.c
riscv64-unknown-linux-gnu-gcc -o test test.o
执行RISCV程序test
oslab@oslab-virtual-machine:/opt/riscv/bin/Linker/dynamic$ qemu-riscv64 test
Welcome to World of PLT and GOT
查看反汇编
riscv64-unknown-linux-gnu-objdump -d test.o
test.o: file format elf64-littleriscv
Disassembly of section .text:
0000000000000000 <print_banner>:
0: 1141 addi sp,sp,-16
2: e406 sd ra,8(sp)
4: e022 sd s0,0(sp)
6: 0800 addi s0,sp,16
8: 000007b7 lui a5,0x0
c: 00078513 mv a0,a5
10: 00000097 auipc ra,0x0
14: 000080e7 jalr ra # 10 <print_banner+0x10>
18: 0001 nop
1a: 60a2 ld ra,8(sp)
1c: 6402 ld s0,0(sp)
1e: 0141 addi sp,sp,16
20: 8082 ret
0000000000000022 <main>:
22: 1141 addi sp,sp,-16
24: e406 sd ra,8(sp)
26: e022 sd s0,0(sp)
28: 0800 addi s0,sp,16
2a: 00000097 auipc ra,0x0
2e: 000080e7 jalr ra # 2a <main+0x8>
32: 4781 li a5,0
34: 853e mv a0,a5
36: 60a2 ld ra,8(sp)
38: 6402 ld s0,0(sp)
3a: 0141 addi sp,sp,16
3c: 8082 ret
riscv64-unknown-linux-gnu-objdump -d test > test.asm
Disassembly of section .plt:
0000000000010430 <_PROCEDURE_LINKAGE_TABLE_>:
10430: 97 23 00 00 33 03 c3 41 03 be 03 bc 13 03 43 fd .#..3..A......C.
10440: 93 82 03 bc 13 53 13 00 83 b2 82 00 67 00 0e 00 .....S......g...
0000000000010450 <__libc_start_main@plt>:
10450: 00002e17 auipc t3,0x2
10454: bb0e3e03 ld t3,-1104(t3) # 12000 <__libc_start_main@GLIBC_2.34>
10458: 000e0367 jalr t1,t3
1045c: 00000013 nop
0000000000010460 <puts@plt>:
10460: 00002e17 auipc t3,0x2
10464: ba8e3e03 ld t3,-1112(t3) # 12008 <puts@GLIBC_2.27>
10468: 000e0367 jalr t1,t3
1046c: 00000013 nop
可以看到,除了第一个plt表项外,每个plt 表都是跳转到对应的 got 表项
对test使用qemu-riscv64和riscv64-unknown-linux-gnu-gdb进行调试
qemu-riscv64 -singlestep -g 1234 test
oslab@oslab-virtual-machine:/opt/riscv/bin/Linker/dynamic$
riscv64-unknown-linux-gnu-gdb ./test
GNU gdb (GDB) 14.2
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "--host=x86_64-pc-linux-gnu --target=riscv64-unknown-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./test...
(gdb) target remote localhost:1234
Remote debugging using localhost:1234
Reading symbols from /opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1...
(No debugging symbols found in /opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1)
0x00000040008112e2 in _start () from /opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1
(gdb) b main
Breakpoint 1 at 0x1052c: file test.c, line 8.
(gdb) c
Continuing.
之后再test.asm中
得到got表项的地址,在gdb中查看
(gdb) x/x 0x12008
0x12008 <puts@got.plt>: 0x00010430
可以看到此处的puts的got表项的地址就是plt表的0x10430
接着调试执行
(gdb) disas
Dump of assembler code for function puts@plt:
0x0000000000010460 <+0>: auipc t3,0x2
0x0000000000010464 <+4>: ld t3,-1112(t3) # 0x12008 <puts@got.plt>
=> 0x0000000000010468 <+8>: jalr t1,t3
0x000000000001046c <+12>: nop
End of assembler dump.
(gdb) info r t3
t3 0x10430 66608
(gdb) si
0x0000000000010430 in _PROCEDURE_LINKAGE_TABLE_ ()
(gdb) info r t3
t3 0x10430 66608
(gdb) disas
Dump of assembler code for function _PROCEDURE_LINKAGE_TABLE_:
=> 0x0000000000010430 <+0>: auipc t2,0x2
0x0000000000010434 <+4>: sub t1,t1,t3
0x0000000000010438 <+8>: ld t3,-1088(t2) # 0x11ff0
0x000000000001043c <+12>: addi t1,t1,-44
0x0000000000010440 <+16>: addi t0,t2,-1088
0x0000000000010444 <+20>: srli t1,t1,0x1
0x0000000000010448 <+24>: ld t0,8(t0)
0x000000000001044c <+28>: jr t3
End of assembler dump.
最终要跳转到t3处的值,也就是_dl_runtime_resolve函数
0x000000400080d7b4 in _dl_runtime_resolve ()
from /opt/riscv/sysroot/lib/ld-linux-riscv64-lp64d.so.1
(gdb) disas
Dump of assembler code for function _dl_runtime_resolve:
=> 0x000000400080d7b4 <+0>: addi sp,sp,-144
0x000000400080d7b6 <+2>: sd ra,72(sp)
0x000000400080d7b8 <+4>: sd a0,8(sp)
0x000000400080d7ba <+6>: sd a1,16(sp)
0x000000400080d7bc <+8>: sd a2,24(sp)
0x000000400080d7be <+10>: sd a3,32(sp)
0x000000400080d7c0 <+12>: sd a4,40(sp)
0x000000400080d7c2 <+14>: sd a5,48(sp)
0x000000400080d7c4 <+16>: sd a6,56(sp)
0x000000400080d7c6 <+18>: sd a7,64(sp)
0x000000400080d7c8 <+20>: fsd fa0,80(sp)
0x000000400080d7ca <+22>: fsd fa1,88(sp)
0x000000400080d7cc <+24>: fsd fa2,96(sp)
0x000000400080d7ce <+26>: fsd fa3,104(sp)
0x000000400080d7d0 <+28>: fsd fa4,112(sp)
0x000000400080d7d2 <+30>: fsd fa5,120(sp)
0x000000400080d7d4 <+32>: fsd fa6,128(sp)
0x000000400080d7d6 <+34>: fsd fa7,136(sp)
0x000000400080d7d8 <+36>: slli a1,t1,0x1
0x000000400080d7dc <+40>: mv a0,t0
0x000000400080d7de <+42>: add a1,a1,t1
0x000000400080d7e0 <+44>: auipc a2,0x16
0x000000400080d7e4 <+48>: ld a2,-2040(a2) # 0x4000822fe8
0x000000400080d7e8 <+52>: jalr a2
0x000000400080d7ea <+54>: mv t1,a0
0x000000400080d7ec <+56>: ld ra,72(sp)
0x000000400080d7ee <+58>: ld a0,8(sp)
0x000000400080d7f0 <+60>: ld a1,16(sp)
0x000000400080d7f2 <+62>: ld a2,24(sp)
0x000000400080d7f4 <+64>: ld a3,32(sp)
0x000000400080d7f6 <+66>: ld a4,40(sp)
0x000000400080d7f8 <+68>: ld a5,48(sp)
--Type <RET> for more, q to quit, c to continue without paging--
0x000000400080d7fa <+70>: ld a6,56(sp)
0x000000400080d7fc <+72>: ld a7,64(sp)
0x000000400080d7fe <+74>: fld fa0,80(sp)
0x000000400080d800 <+76>: fld fa1,88(sp)
0x000000400080d802 <+78>: fld fa2,96(sp)
0x000000400080d804 <+80>: fld fa3,104(sp)
0x000000400080d806 <+82>: fld fa4,112(sp)
0x000000400080d808 <+84>: fld fa5,120(sp)
0x000000400080d80a <+86>: fld fa6,128(sp)
0x000000400080d80c <+88>: fld fa7,136(sp)
0x000000400080d80e <+90>: addi sp,sp,144
0x000000400080d810 <+92>: jr t1
End of assembler dump.
基本调用流程
1.XX@plt
2.xx@got
3.plt表段起始地址
4._dl_runtime_resolve ()
到这里我们还需要知道
- _dl_runtime_resolve 是怎么知道要查找 printf 函数的
- _dl_runtime_resolve 找到 printf 函数地址之后,它怎么知道回填到哪个 GOT 表项
查看重定位信息
oslab@oslab-virtual-machine:/opt/riscv/bin/Linker/dynamic$ riscv64-unknown-linux-gnu-readelf -r test
Relocation section '.rela.plt' at offset 0x3f8 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000012000 000300000005 R_RISCV_JUMP_SLOT 0000000000010450 __libc_start_main@GLIBC_2.34 + 0
000000012008 000100000005 R_RISCV_JUMP_SLOT 0000000000010460 puts@GLIBC_2.27 + 0
3.快速链接
修改源码,二次调用printf函数
#include <stdio.h>
void print_banner()
{
printf("Welcome to World of PLT and GOT\n");
}
int main(void)
{
print_banner();
print_banner();
return 0;
}
其他步骤如上
比较两次的got表项的值
//一开始没有重定位的时候将 printf@got 填成 lookup_printf 的地址
void printf@plt()
{
address_good:
jmp *printf@got
lookup_printf:
调用重定位函数查找 printf 地址,并写到 printf@got
goto address_good;//再返回去执行address_good
}
gdb) disas
Dump of assembler code for function puts@plt:
0x0000000000010460 <+0>: auipc t3,0x2
0x0000000000010464 <+4>: ld t3,-1112(t3) # 0x12008 <puts@got.plt>
=> 0x0000000000010468 <+8>: jalr t1,t3
0x000000000001046c <+12>: nop
End of assembler dump.
(gdb) info r t3
t3 0x10430 66608
(gdb) c
Continuing.
Breakpoint 2, 0x0000000000010516 in print_banner () at test.c:4
4 printf("Welcome to World of PLT and GOT\n");
(gdb) c
Continuing.
Breakpoint 3, 0x0000000000010468 in puts@plt ()
(gdb) disas
Dump of assembler code for function puts@plt:
0x0000000000010460 <+0>: auipc t3,0x2
0x0000000000010464 <+4>: ld t3,-1112(t3) # 0x12008 <puts@got.plt>
=> 0x0000000000010468 <+8>: jalr t1,t3
0x000000000001046c <+12>: nop
End of assembler dump.
(gdb) info r t3
t3 0x40008961a8 274886910376
(gdb) x 0x40008961a8
0x40008961a8 <puts>: 0xe44e7179
(gdb) disas 0x40008961a8
Dump of assembler code for function puts:
0x00000040008961a8 <+0>: addi sp,sp,-48
0x00000040008961aa <+2>: sd s3,8(sp)
0x00000040008961ac <+4>: auipc s3,0xee
0x00000040008961b0 <+8>: ld s3,-916(s3) # 0x4000983e18
0x00000040008961b4 <+12>: sd s0,32(sp)
0x00000040008961b6 <+14>: sd s1,24(sp)
0x00000040008961b8 <+16>: sd s2,16(sp)
0x00000040008961ba <+18>: sd ra,40(sp)
0x00000040008961bc <+20>: sd s4,0(sp)
0x00000040008961be <+22>: mv s2,a0
0x00000040008961c0 <+24>: jal 0x40008b47da <strlen>
0x00000040008961c4 <+28>: ld s1,0(s3)
0x00000040008961c8 <+32>: mv s0,a0
0x00000040008961ca <+34>: lw a5,0(s1)
0x00000040008961cc <+36>: slli a4,a5,0x30
0x00000040008961d0 <+40>: bgez a4,0x400089626c <puts+196>
0x00000040008961d4 <+44>: mv a0,s1
0x00000040008961d6 <+46>: lw a5,192(a0)
0x00000040008961da <+50>: bnez a5,0x4000896262 <puts+186>
0x00000040008961dc <+52>: li a5,-1
0x00000040008961de <+54>: sw a5,192(a0)
0x00000040008961e2 <+58>: ld s4,216(a0)
0x00000040008961e6 <+62>: lui a5,0x1
0x00000040008961e8 <+64>: auipc a4,0xed
0x00000040008961ec <+68>: addi a4,a4,-80 # 0x4000983198 <__io_vtables>
0x00000040008961f0 <+72>: sub a4,s4,a4
0x00000040008961f4 <+76>: addi a5,a5,-1745 # 0x92f
0x00000040008961f8 <+80>: bltu a5,a4,0x40008962da <puts+306>
0x00000040008961fc <+84>: ld a5,56(s4)
0x0000004000896200 <+88>: mv a2,s0
0x0000004000896202 <+90>: mv a1,s2
0x0000004000896204 <+92>: jalr a5
0x0000004000896206 <+94>: bne s0,a0,0x4000896268 <puts+192>
第一次
第二次快速链接
调试程序的步骤
没有配置好riscv64-unknown-linux-gnu-gdb和qemu相结合的调试环境
已完成
qemu-riscv64 -singlestep -g 1234 Hello
riscv64-unknown-elf-gdb Hello
#进入gdb调试模式后,需要执行target remote localhost:端口号 来连接qemu模拟器:
(gdb)target remote localhost:1234
#之后再gdb就可以正常调试RISCV程序了
链接器通常需要为每个符号调整两条 RV32I 指令。如
数据地址需要调整 lui 和 addi
代码地址需要调整 auipc 和 jalr。
链接器松弛(没太看懂)
Linker Relaxation in LLD - Chih-Mao Chen, Andes Technology - YouTube
Linker Relaxation - 知乎 (zhihu.com)
加载器
由操作系统实现