Skip to the content.

RISC-V中的寻址模式及链接器中的Relocation和Relaxation

本文介绍RISC-V中的寻址模式,包括跳转指令和LOAD/STORE指令的寻址模式。然后介绍链接器中静态链接时的Relocation和Relaxation,以及链接器对RISC-V中的寻址模式的使用。链接过程对最终代码生成会有影响,有时并不能只看汇编,尤其做Benchmark对比的时候需要考虑这块的影响。

RISC-V中的寻址模式介绍

RV32I中直接跟地址相关的指令包括:

另外RV64I中新增的跟地址相关的指令包括ld/sd,跟RV32I中的LOAD/STORE类似。所以可以认为RISC-V包括两种寻址模式,一种是PC相关寻址(jal、BRANCH),一种是寄存器寻址(jalr、LOAD/STORE)。不过实际上寄存器寻址可以再分成PC和绝对地址两种,这是通过结合auipclui指令来实现,如下面的汇编所示。因此,RISC-V中实际上是支持PC相关和绝对地址两种寻址模式,其中jal和BRANCH指令只支持PC相关寻址,jalr和LOAD/STORE指令两种寻址模式都支持。

;; 跳转到PC相关的地址,基于当前PC做偏移
auipc t1, imm20
jalr t2, t1, imm12

;; 跳转到绝对地址,可以看做相对于0做偏移
lui t1, imm20
jalr t2, t1, imm12

auipclui指令介绍:

目标文件中的Relocation和Relaxation

Relocation(重定向)是指为位置相关代码和数据分配实际地址并对应修改相关代码的一个过程(Wikipedia)。那么为什么需要Relocation呢?回答这个问题需要先来看下GCC是如何生成可执行文件的。GCC总共分成四个阶段来生成可执行文件(通过--save-temps可以看到每个阶段的输出):

正是因为负责生成指令的编译阶段和负责确定各个符号地址的链接阶段是分开的。当一个.c文件使用到了另外一个.c文件中的某个变量时,在编译阶段是无法确定该变量的内存地址的,只有到链接阶段才能知道。因此需要有一个东西来串联起来,以便告知链接器编译出的哪些指令需要在链接的时候进行修正。这就是Relocation所要做的事情。通过Relocation表,链接器知道有哪些地址需要被修正,并在确定了所引用符号的最终地址后将其修正过来。而Relaxation表的作用在于提醒链接器,可以尝试优化指令以提高性能(比如减少不必要的指令)。能进行优化的前提是前面提到的RISC-V中提供的各种地址相关的各种指令,比如当地址不超过imm12范围时,可以删除不必要的aupiclui指令。下面来详细介绍一下。

根据当前的RISC-V ABI规范,静态链接时存在30多种Relocations,本文主要介绍R_RISCV_HI20+R_RISCV_LO12_I/R_RISCV_LO12_S、R_RISCV_PCREL_HI20+R_RISCV_PCREL_LO12_I/R_RISCV_PCREL_LO12_S、R_RISCV_BRANCH、R_RISCV_CALL、R_RISCV_JAL这几种Relocations,并会使用实际的例子来展示。想了解更详细的有关Relocations的信息请移步RISC-V ELF Specification

影响Relocations生成的GCC编译器参数如下:

后面通过下面的示例代码来查看生成的Relocations:

Relocation及其使用过程

命令:riscv64-unknown-linux-gnu-gcc -march=rv64g -mcmodel=medlow -mno-relax -nostartfiles --save-temp -Wl,-q relocation.c main.c

参数解释:

使用上面的命令编译生成可执行文件,同时将中间文件保存下来。比如a-relocation.o就是relocation.c对应的目标文件,这里的前缀a-实际上是最终可执行文件的名称(a.out中的a)。

medlow对地址有限制,如果加上参数-Wl,-Ttext-segment,0x80000000,会导致报错(riscv64-unknown-linux-gnu-gcc -march=rv64g -mcmodel=medlow -mno-relax -nostartfiles --save-temp -Wl,-q relocation.c main.c -Wl,-Ttext-segment,0x80000000):

a-relocation.o: in function `bar':
relocation.c:(.text+0x2c): relocation truncated to fit: R_RISCV_HI20 against symbol `global_var' defined in .sdata section in a-main.o
collect2: error: ld returned 1 exit status

报错的意思是说global_var符号的值超过了R_RISCV_HI20+R_RISCV_LO12_I能表示的范围了,会被截断。这时就需要使用medany 的Code Model了。即将-cmodel=medlow改为-cmodel=medany。下面来看下medany对Relocation的影响。

命令:riscv64-unknown-linux-gnu-gcc -march=rv64g -mcmodel=medany -mno-relax -nostartfiles --save-temp -Wl,-q relocation.c main.c

这里重点看-mcmodel=medany-mcmodel=medlow 对Relocation计算的区别,这里主要的原因是寻址范围不同。

Relaxation及其使用过程

从前面的例子会发现,不管是哪种Code Model,有些Relocation可以转换成指令更少的Relocation,比如将 R_RISCV_CALL 转换成 R_RISCV_JAL。为了指导链接器做相应的优化,需要带上Relaxation标记,也就是 R_RISCV_RELAX 类型的Relocation。这种标记不能单独存在,需要跟在正常的Relocation后面。

将之前的命令去掉-mno-relax参数:riscv64-unknown-linux-gnu-gcc -march=rv64g -mcmodel=medlow -nostartfiles --save-temp -Wl,-q relocation.c main.c

然后dump a-relocation.o文件会发现 R_RISCV_CALL 和 R_RISCV_HI20+R_RISCV_LO12_I 下面都带上了 R_RISCV_RELAX 。进一步dump a.out会发现 R_RISCV_CALL 被替换成了 R_RISCV_JAL。而 R_RISCV_HI20+R_RISCV_LO12_I 对应的符号因为范围超过了imm12表示的范围,所以没有被优化。

# riscv64-unknown-linux-gnu-objdump -d -r -Mno-aliases a-relocation.o

...
  20:   00000097                auipc   ra,0x0
                        20: R_RISCV_CALL        foo
                        20: R_RISCV_RELAX       *ABS*
  24:   000080e7                jalr    ra,0(ra) # 20 <bar+0x20>
  28:   fea43423                sd      a0,-24(s0)
  2c:   000007b7                lui     a5,0x0
                        2c: R_RISCV_HI20        global_var
                        2c: R_RISCV_RELAX       *ABS*
  30:   0007b783                ld      a5,0(a5) # 0 <bar>
                        30: R_RISCV_LO12_I      global_var
                        30: R_RISCV_RELAX       *ABS*
  34:   fe843703                ld      a4,-24(s0)
...
# riscv64-unknown-linux-gnu-objdump -d -r -Mno-aliases a.out

...
   102a0:       05c000ef                jal     ra,102fc <foo>
                        102a0: R_RISCV_JAL      foo
                        102a0: R_RISCV_RELAX    *ABS*
   102a4:       fea43423                sd      a0,-24(s0)
   102a8:       000127b7                lui     a5,0x12
                        102a8: R_RISCV_HI20     global_var
                        102a8: R_RISCV_RELAX    *ABS*
   102ac:       0087b783                ld      a5,8(a5) # 12008 <global_var>
                        102ac: R_RISCV_LO12_I   global_var
                        102ac: R_RISCV_RELAX    *ABS*
   102b0:       fe843703                ld      a4,-24(s0)
...

R_RISCV_CALL 的 Relaxation 过程可以从binutils的源码中看到,源代码中的注释说的很清楚,如下所示。

根据跳转的范围,R_RISCV_CALL 总共有三种 Relaxation 路径:

/* 源代码在线地址:https://github.com/bminor/binutils-gdb/blob/4ed07377e47addf4dd0594ac5b16d7e4cdb19436/bfd/elfnn-riscv.c#L4171 */

/* Relax AUIPC + JALR into JAL.  */

static bool
_bfd_riscv_relax_call (bfd *abfd, asection *sec, asection *sym_sec,
		       struct bfd_link_info *link_info,
		       Elf_Internal_Rela *rel,
		       bfd_vma symval,
		       bfd_vma max_alignment,
		       bfd_vma reserve_size ATTRIBUTE_UNUSED,
		       bool *again,
		       riscv_pcgp_relocs *pcgp_relocs,
		       bool undefined_weak ATTRIBUTE_UNUSED)
{
  bfd_byte *contents = elf_section_data (sec)->this_hdr.contents;
  bfd_vma foff = symval - (sec_addr (sec) + rel->r_offset);
  bool near_zero = (symval + RISCV_IMM_REACH / 2) < RISCV_IMM_REACH;
  bfd_vma auipc, jalr;
  int rd, r_type, len = 4, rvc = elf_elfheader (abfd)->e_flags & EF_RISCV_RVC;

  /* If the call crosses section boundaries, an alignment directive could
     cause the PC-relative offset to later increase, so we need to add in the
     max alignment of any section inclusive from the call to the target.
     Otherwise, we only need to use the alignment of the current section.  */
  if (VALID_JTYPE_IMM (foff))
    {
      if (sym_sec->output_section == sec->output_section
	  && sym_sec->output_section != bfd_abs_section_ptr)
	max_alignment = (bfd_vma) 1 << sym_sec->output_section->alignment_power;
      foff += ((bfd_signed_vma) foff < 0 ? -max_alignment : max_alignment);
    }

  /* See if this function call can be shortened.  */
  if (!VALID_JTYPE_IMM (foff) && !(!bfd_link_pic (link_info) && near_zero))
    return true;

  /* Shorten the function call.  */
  BFD_ASSERT (rel->r_offset + 8 <= sec->size);

  auipc = bfd_getl32 (contents + rel->r_offset);
  jalr = bfd_getl32 (contents + rel->r_offset + 4);
  rd = (jalr >> OP_SH_RD) & OP_MASK_RD;
  rvc = rvc && VALID_CJTYPE_IMM (foff);

  /* C.J exists on RV32 and RV64, but C.JAL is RV32-only.  */
  rvc = rvc && (rd == 0 || (rd == X_RA && ARCH_SIZE == 32));

  if (rvc)
    {
      /* Relax to C.J[AL] rd, addr.  */
      r_type = R_RISCV_RVC_JUMP;
      auipc = rd == 0 ? MATCH_C_J : MATCH_C_JAL;
      len = 2;
    }
  else if (VALID_JTYPE_IMM (foff))
    {
      /* Relax to JAL rd, addr.  */
      r_type = R_RISCV_JAL;
      auipc = MATCH_JAL | (rd << OP_SH_RD);
    }
  else
    {
      /* Near zero, relax to JALR rd, x0, addr.  */
      r_type = R_RISCV_LO12_I;
      auipc = MATCH_JALR | (rd << OP_SH_RD);
    }

  /* Replace the R_RISCV_CALL reloc.  */
  rel->r_info = ELFNN_R_INFO (ELFNN_R_SYM (rel->r_info), r_type);
  /* Replace the AUIPC.  */
  riscv_put_insn (8 * len, auipc, contents + rel->r_offset);

  /* Delete unnecessary JALR.  */
  *again = true;
  return riscv_relax_delete_bytes (abfd, sec, rel->r_offset + len, 8 - len,
				   link_info, pcgp_relocs);
}

总结

Relocation的存在是因为编译和链接过程是分开进行的,需要有一个机制在两者之间进行信息传递(也就是在ELF中包含Relocation段),告诉链接器如何修正地址。Relaxation的存在是跟RISC-V的指令集架构有关系,正是因为RISC-V寻址范围受限,才有必要通过Relaxation来显式的告诉链接器主动减少不必要的指令,从而优化程序的性能和Code Size。

参考