RISCV 编译 Link探究

RISCV 编译 Link探究

问题: 如何将代码编译到指定的mem地址?

在用$RISCV/bin/riscv64-unknown-elf-gcc hello.c时生成的目标文件默认的都是从0x0000_1000地址开始,
如何自定义指定到从0x8000_0000地址开始?
那么就得熟悉连接器的原理

默认链接脚本

gcc在编译和链接的时候,如果不指定链接脚本,会指向默认的链接脚本
默认的链接脚本可以用 $RISCV/bin/riscv64-unknown-elf-ld -verbose 来查看

如何更改链接脚本

1
2
3
4
5
6
7
8
SECTIONS
{
. = 0x8000000;
.text : { *(.text) }
. = 0x9000000;
.data : { *(.data) }
.bss : { *(.bss) }
}

在编译时指定$RISCV/bin/riscv64-unknown-elf-gcc -T link.lds hello.c -o hello.out
默认会报错,没有指定_start 就没有办法分配堆和栈空间

1
2
3
4
5
xxxx/bin/../lib/gcc/riscv64-unknown-elf/8.1.0/../../../../riscv64-unknown-elf/lib/rv64imafdc/lp64d/crt0.o: In function `_start':
(.text+0x0): undefined reference to `__global_pointer$'
(.text+0x8): undefined reference to `_edata'
(.text+0x10): undefined reference to `_end'
collect2.exe: error: ld returned 1 exit status

简单起见添加option -nostartfiles 忽略指定_start
$RISCV/bin/riscv64-unknown-elf-gcc -T link.lds hello.c -o hello.out -nostartfiles
然后$RISCV/bin/riscv64-unknown-elf-objdump -d hello.out > hello.dump可以看到.text 从0x8000000地址开始

1
2
3
4
5
6
7
8
9
hello.out:     file format elf64-littleriscv

Disassembly of section .text:

0000000008000000 <main>:
8000000: 1101 addi sp,sp,-32
8000002: ec06 sd ra,24(sp)
8000004: e822 sd s0,16(sp)
8000006: 1000 addi s0,sp,32

完整的参考实例

参见hbird-e-sdk是RISCV开源E200系列开发板的sdk

https://github.com/SI-RISCV/hbird-e-sdk

./generate_test4sim.csh 生成指定地址的out以及.verilog 二进制可读文件

编译新的case只需
make dasm PROGRAM=hellowrold BOARD=hbird-e200 CORE=e203 DOWNLOAD=itcm即可
展看来看,实际上执行的是

1
2
3
4
5
6
7
8
9
10
11
12
make -C software/hello_world  \
SIZE=$(RISCV_GNU_TOOLCHAIN)/bin/riscv-none-embed-size \
CC=$(RISCV_GNU_TOOLCHAIN)/bin/riscv-none-embed-gcc \
RISCV_ARCH=rv32imac \
REPLACE_PRINTF=0 \
NANO_PFLOAT=1 \
USE_NANO=1 \
DOWNLOAD=itcm \
RISCV_ABI=ilp32 \
AR=$(RISCV_GNU_TOOLCHAIN)/bin/riscv-none-embed-ar \
BSP_BASE=/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp \
BOARD=hbird-e200 \

具体执行的是hbird-e-sdk/Makefile下的makefile,具体过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# cd /lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/software/hello_world

$(RISCV_GNU_TOOLCHAIN)/bin/riscv-none-embed-gcc -O2 -g -march=rv32imac -mabi=ilp32 \
-ffunction-sections -fdata-sections -fno-common \
-I/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs \
-I/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/drivers \
-I/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/env \
-I/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/include \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/env/start.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/env/entry.o \
hello_world.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/env/init.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/close.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/_exit.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/write_hex.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/fstat.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/isatty.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/lseek.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/read.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/sbrk.o \
/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/stubs/write.o \
-o hello_world \
-T /lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/env/link_itcm.lds -nostartfiles \
-Wl,--gc-sections -Wl,--check-sections --specs=nano.specs -u _printf_float \
-L/lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/bsp/hbird-e200/env \

$(RISCV_GNU_TOOLCHAIN)/bin/riscv-none-embed-size hello_world
# Leaving /lifespace/t1/jijing/lab/e200_opensource/hbird-e-sdk/software/hello_world

还可以在bsp/env/common.k 中添加一行查看C编译成的hello.s汇编程序文件

1
2
3
4
$(TARGET): $(LINK_OBJS) $(LINK_DEPS)
$(CC) $(CFLAGS) $(INCLUDES) $@.c -S #添加该行,可以在对于目录下生成 .s汇编程序
$(CC) $(CFLAGS) $(INCLUDES) $(LINK_OBJS) -o $@ $(LDFLAGS)
$(SIZE) $@

具体link脚本先关细节可见:
hbird-e-sdk/bsp/hbird-e200/env/common.mk 文件中 LDFLAGS +=的定义
对应脚本

  • hbird-e-sdk/bsp/hbird-e200/env/link_itcm.lds 下载目标ITCM 链接细节的描述
  • hbird-e-sdk/bsp/hbird-e200/env/link_flash.lds 下载目标Flash 链接细节的描述
  • hbird-e-sdk/bsp/hbird-e200/env/link_flashxip.lds

编译好的二进制可读文件在e200的vsim目录下
make run_test TESTCASE=$PWD/../riscv-tools/fpga_test4sim/hello4sim/hello_world 即可启动仿真环境

lds 链接脚本的原理介绍

完整的文章

链接器把一个或多个输入文件合成一个输出文件.
输入文件: 目标文件或链接脚本文件.
输出文件: 目标文件或可执行文件.

目标文件(包括可执行文件)具有固定的格式, 在UNIX或GNU/Linux平台下, 一般为ELF格式
有时把输入文件内的section称为输入section(input section), 把输出文件内的section称为输出section(output sectin).
目标文件的每个section至少包含两个信息: 名字和大小. 大部分section还包含与它相关联的一块数据, 称为section contents(section内容). 一个section可被标记为“loadable(可加载的)”或“allocatable(可分配的)”.
loadable section: 在输出文件运行时, 相应的section内容将被载入进程地址空间中.
allocatable section: 内容为空的section可被标记为“可分配的”. 在输出文件运行时, 在进程地址空间中空出大小同section指定大小的部分. 某些情况下, 这块内存必须被置零.
如果一个section不是“可加载的”或“可分配的”, 那么该section通常包含了调试信息. 可用objdump -h命令查看相关信息.
每个“可加载的”或“可分配的”输出section通常包含两个地址:

  • VMA(virtual memory address虚拟内存地址或程序地址空间地址)
  • LMA(load memory address加载内存地址或进程地址空间地址). 通常VMA和LMA是相同的.

在目标文件中, loadable或allocatable的输出section有两种地址: VMA(virtual Memory Address)和LMA(Load Memory Address). VMA是执行输出文件时section所在的地址, 而LMA是加载输出文件时section所在的地址. 一般而言, 某section的VMA == LMA. 但在嵌入式系统中, 经常存在加载地址和执行地址不同的情况: 比如将输出文件加载到开发板的flash中(由LMA指定), 而在运行时将位于flash中的输出文件复制到SDRAM中(由VMA指定).
可这样来理解VMA和LMA, 假设:

  • (1) .data section对应的VMA地址是0×08050000, 该section内包含了3个32位全局变量, i、j和k, 分别为1,2,3.
  • (2) .text section内包含由”printf( “j=%d “, j );”程序片段产生的代码.

连接时指定.data section的VMA为0×08050000, 产生的printf指令是将地址为0×08050004处的4字节内容作为一个整数打印出来。
如果.data section的LMA为0×08050000,显然结果是j=2
如果.data section的LMA为0×08050004,显然结果是j=1
还可这样理解LMA:
.text section内容的开始处包含如下两条指令(intel i386指令是10字节,每行对应5字节):
jmp 0×08048285
movl $0×1,%eax
如果.text section的LMA为0×08048280, 那么在进程地址空间内0×08048280处为“jmp 0×08048285”指令, 0×08048285处为movl $0×1,%eax指令. 假设某指令跳转到地址0×08048280, 显然它的执行将导致%eax寄存器被赋值为1.
如果.text section的LMA为0×08048285, 那么在进程地址空间内0×08048285处为“jmp 0×08048285”指令, 0×0804828a处为movl $0×1,%eax指令. 假设某指令跳转到地址0×08048285, 显然它的执行又跳转到进程地址空间内0×08048285处, 造成死循环.
符号(symbol): 每个目标文件都有符号表(SYMBOL TABLE), 包含已定义的符号(对应全局变量和static变量和定义的函数的名字)和未定义符号(未定义的函数的名字和引用但没定义的符号)信息.
符号值: 每个符号对应一个地址, 即符号值(这与c程序内变量的值不一样, 某种情况下可以把它看成变量的地址). 可用nm命令查看它们. (nm的使用方法可参考本blog的GNU binutils笔记)

脚本格式

链接脚本由一系列命令组成, 每个命令由一个关键字(一般在其后紧跟相关参数)或一条对符号的赋值语句组成. 命令由分号‘;’分隔开.
文件名或格式名内如果包含分号’;’或其他分隔符, 则要用引号‘”’将名字全称引用起来. 无法处理含引号的文件名./ /之间的是注释。

# riscv

评论