回顾: 从C代码到指令序列
预处理 -> 编译 -> 汇编 -> 链接 -> 执行
本次课内容:
代码 + 数据
没有了吗?
有很多实际的需求
Enable debug information
会影响NEMU的大小addr2line
可以根据调试信息, 将地址翻译到源文件位置
需要更多的数据
ld - the GNU linker.
as - the GNU assembler.
addr2line - Converts addresses into filenames and line numbers.
ar - A utility for creating, modifying and extracting from archives.
c++filt - Filter to demangle encoded C++ symbols.
dlltool - Creates files for building and using DLLs.
elfedit - Allows alteration of ELF format files.
gprof - Displays profiling information.
gprofng - Collects and displays application performance data.
nlmconv - Converts object code into an NLM.
nm - Lists symbols from object files.
objcopy - Copies and translates object files.
objdump - Displays information from object files.
ranlib - Generates an index to the contents of an archive.
readelf - Displays information from any ELF format object file.
size - Lists the section sizes of an object or archive file.
strings - Lists printable strings from files.
strip - Discards symbols.
对系统程序员很有用
objcopy
可以生成Verilog中readmemh()
的输入需求: 代码和数据分离
节 | 说明 | | | 节 | 说明 |
---|---|---|---|---|
.text | 代码 | | | .bss | 未初始化数据 |
.data | 可写数据 | | | .debug | 调试信息 |
.rodata | 只读数据 | | | .line | 行号信息 |
.bss - Block Starting Symbol的缩写(section.c)
C99.6.7.8 #10 - 未初始化的全局/静态变量的初值为0
If an object that has automatic storage duration is not initialized explicitly, its
value is indeterminate. If an object that has static storage duration is not
initialized explicitly, then:
- if it has pointer type, it is initialized to a null pointer;
- if it has arithmetic type, it is initialized to (positive or unsigned) zero;
- if it is an aggregate, every member is initialized (recursively) according to these
rules;
- if it is a union, the first named member is initialized (recursively) according to
these rules.
Q1: 如果节的名称太长, 怎么办?
A1: 把元数据的字符串集中放在另一个区域
name
换成一个 “指针”: 字符串在文件中的位置.strtab
(string
table)Q2: 可以通过节头找到一个节, 那该如何找到节头?
A2: 约定一个位置
Q3: 找到第一个节头后, 如何找到下一个?
A3: 需要考虑如何组织多个节头?
RTFM
man 5 elf
预处理 -> 编译 -> 汇编 -> 链接 -> 执行
riscv64-linux-gnu-gcc -march=rv64g -fno-pic -c -O2 main.c
riscv64-linux-gnu-gcc -march=rv64g -fno-pic -c -O2 f.c
编译和汇编均以文件为单位进行,
标记*
的指令/数据无法得知最终地址
袁春风, 南京大学《计算机系统基础》第四章 程序的链接
为了让链接器解析符号, 汇编器需将符号信息记录到ELF目标文件中
.symtab
节 - 符号表(symbol table),
每一个表项记录一个符号的信息
.strtab
节中的位置UND
- 该符号未解析链接器维护两个集合: D(已定义的符号)和U(未解析的符号)
链接器扫描参与链接的文件, 查看其符号表, 分析每个符号的定义情况
所属节有效 | 所属节为UND |
|
---|---|---|
已在D中 | 多重定义错误 | 解析为D中的x |
已在U中 | 将U中的x解析为当前x, 并加入D | 加入U |
均不在 | 加入D | 加入U |
multiple definition of xxx
undefined reference to xxx
但如果链接库函数, 就会遇到新的问题(假设不支持动态链接)
.o
文件中
/usr/lib/x86_64-linux-gnu/libc-*.so
接近2MB.o
打包成一个.a
文件,
用户只需要指定这个.a
文件.o
文件,
没有用到的.o
文件不参与链接原因: 链接大量文件时开销较大
.o
文件.o
文件-(
和-)
启用多趟解析(时间开销增大),
具体查阅man ld
开启-fcommon
时,
gcc将试探性定义的符号放到目标文件的COMMON块
COM
符号解析时
试探性定义是C标准定义的, 但在实际开发中是个大坑
-fno-common
选项,
将这种变量直接放到.bss
-fcommon
static
修饰(局部符号不参与符号解析); 再否则
extern
修饰x86-qemu
的AM定义了一个叫cpus
的全局变量,
把做oslab的同学坑惨了 😂合并节后, 节中的符号相对于该节的位置会有变动
同时也需要重填引用这些符号的位置
.relxxx
和.relaxxx
节
xxx
是需要重填的节名称不同的重定位类型决定了解不同的方程, 填不同形状的坑
R_RISCV_HI20
/R_RISCV_LO12_I
/R_RISCV_LO12_S
/…以f.o
中访问pa
变量为例,
需要计算offset
, 使得以下assert成立:
算出offset
后,
按照lui
和ld
(I型立即数)的指令格式填进去即可
0x10
的sw
指令要填S型立即数
如果f()
距离调用处很近,
感觉用一条jal
指令就可以了?
offset
可以用20位有符号立即数表示,
就可以省一条指令了
R_RISCV_RELAX
来指示
其他松弛的例子:
auipc ra, 0
+ jalr ra, ra, 0
->
jal ra, offset
auipc ra, 0
+ jalr ra, ra, 0
->
c.jal ra, offset
auipc ra, 0
+ jalr x0, ra, 0
->
c.j ra, offset
lui t0, 0
+ lw t1, 0(t0)
->
lw t1, offset(gp)
auipc
+jalr
可以寻址当前PC±2GB的范围
因此, 虽然地址有64位, 但一般来说少量指令能寻址的范围没那么远
这就带来一个权衡: 代码大小 vs. 寻址范围
RISC-V目前常用的code model有
medlow
- 程序位于0±2GB范围,
可通过lui
寻址medany
- 程序位于任意位置±2GB范围,
可通过auipc
寻址-mcmodel=xxx
来选择(RTFM)x86-64还有一个large的code model, 编译器对程序位置和大小不做假设
一个例子: 陈同学将RT-Thread移植到RV64I, 提供给第三期”一生一芯”的同学运行
-march=rv64gc
编译, 怎么办?
总结:
readelf
,
objdump
…