
回顾: 从C代码到指令序列
预处理 -> 编译 -> 汇编 -> 链接 -> 执行
AM项目有那么多源文件, 最后如何生成一个可执行文件?
本次课内容:
代码 + 数据
没有了吗?
有很多实际的需求
Enable debug information会影响NEMU的大小addr2line根据调试信息将地址转换为源文件位置(gdb)
需要更多的数据
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换成一个 “指针”
name换成一个偏移量
.strtab(string
table)Q2: 可以通过节头找到一个节, 那该如何找到节头?
A2: 约定一个位置
Q3: 找到第一个节头后, 如何找到下一个?
A3: 需要考虑如何组织多个节头?

RTFM
man 5 elf预处理 -> 编译 -> 汇编 -> 链接 -> 执行
编译和汇编均以文件为单位进行,
标记*的指令/数据无法得知最终地址

袁春风, 南京大学《计算机系统基础》第四章 程序的链接
为了让链接器解析符号, 汇编器需将符号信息记录到ELF目标文件中
.symtab节 - 符号表(symbol table),
每一个表项记录一个符号的信息
.strtab节中的位置UND - 该符号未解析链接器维护两个集合: D(已定义的符号)和U(未解析的符号)
链接器扫描参与链接的文件, 查看其符号表, 分析每个符号的定义情况
x, 按下表进行操作| 所属节确定 | 所属节为UND |
|
|---|---|---|
| 均不在D或U中 | 加入D | 加入U |
| 已在U中 | 将U中的x解析为当前x,
并加入D |
加入U |
| 已在D中 | 多重定义错误 | 解析为D中的x |
multiple definition of xxx
undefined reference to xxx但如果链接库函数, 就会遇到新的问题(假设不支持动态链接)
.o文件中
/usr/lib/x86_64-linux-gnu/libc-*.so接近2MB.o打包成一个.a文件,
用户只需要指定这个.a文件.o文件,
没用到的.o文件不参与链接AM将程序自身的.o,
am-xxx.a和klib-xxx.a链接成可执行文件
原因: 链接大量文件时开销较大
.o文件.o文件-(和-)启用多趟解析(时间开销增大),
具体查阅man ld// main.c
#include <stdio.h>
int a = 0, b = 0;
extern void f(), g();
int main() {
f(); printf("a = %d(0x%08x), b = %d(0x%08x)\n", a, a, b, b);
g(); printf("a = %d(0x%08x), b = %d(0x%08x)\n", a, a, b, b);
return 0;
}
// main.c
#include <stdio.h>
int a; // tentative definition
extern void f();
int main() {
a = 1; f(); printf("in main: a = %d\n", a);
return 0;
}
带-fcommon时,
gcc将试探性定义的符号放到目标文件的COMMON块
COM符号解析时
试探性定义是C标准定义的, 但在实际开发中是个大坑
-fno-common选项,
将这种变量直接放到.bss
-fcommon
static修饰(局部符号不参与符号解析)extern修饰x86-qemu的AM定义了一个叫cpus的全局变量,
把做oslab的同学坑惨了 😂例子: AM的链接脚本
.代表当前地址ENTRY(_start)
ENTRY(_start)
PHDRS { text PT_LOAD; data PT_LOAD; }
SECTIONS {
/* _pmem_start and _entry_offset
are defined in LDFLAGS */
. = _pmem_start + _entry_offset;
.text : {
*(entry)
*(.text*)
} : text
.rodata : { *(.rodata*) }
.data : { *(.data) } : data
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
}
_stack_top = ALIGN(0x1000);
. = _stack_top + 0x8000;
_stack_pointer = .;
_end = .;
_heap_start = ALIGN(0x1000);
}合并节后, 节中的符号相对于该节的位置会有变动
同时也要重填那些引用符号的位置
.relxxx和.relaxxx节
xxx是需要重填的节名称不同的重定位类型需要解不同的方程, 填不同形状的坑
R_RISCV_HI20/R_RISCV_LO12_I/R_RISCV_LO12_S/…以f.o中访问pa变量为例,
需要计算offset, 使得以下assert成立:
算出offset后,
按照lui和lw(I型立即数)的指令格式填进去即可
0x10的sw指令要填S型立即数
如果f距离调用处很近,
感觉用一条jal指令就可以了?
offset可以用21位有符号立即数表示,
就可以省一条指令了R_RISCV_RELAX来指示链接器允许松弛
其他松弛的例子:
auipc ra, 0 + jalr ra, ra, 0 ->
jal ra, offsetauipc ra, 0 + jalr ra, ra, 0 ->
c.jal ra, offsetauipc ra, 0 + jalr x0, ra, 0 ->
c.j ra, offsetlui t0, 0 + lw t1, 0(t0) ->
lw t1, offset(gp)采用call伪指令能编译出auipc+jalr,
能寻址当前PC±2GB的范围
这就带来一个权衡: 代码大小 vs. 寻址范围
| 指令序列 | 寻址范围 |
|---|---|
jal |
当前PC±1MB |
auipc+jalr |
当前PC±2GB |
| 更多指令 | 更大范围 |
code model用来告诉编译器如何选择
RISC-V目前常用的code model有
medlow - 程序位于0±2GB范围,
可通过lui寻址medany - 程序位于任意位置±2GB范围,
可通过auipc寻址-mcmodel=xxx来选择(RTFM)x86-64还有一个叫large的code model, 编译器对程序位置和大小不作假设